# Porch.com Contractor Scraper — Home Service Pro Leads (`scrapersdelight/porch-pro-scraper`) Actor

785,653 home-service pros from Porch.com: business name, trade, phone (99.7% fill), website, street address, lat/lon, rating, reviews, years in business, full services list, ZIP service area and cached state licence records. 131 trades, all 50 states. Filter by trade, state, city or rating.

- **URL**: https://apify.com/scrapersdelight/porch-pro-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 pro 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/actors/running/actors-in-store.md#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

## Porch.com Contractor Scraper — Home Service Pro Leads

Home-service professionals from [Porch.com](https://pro.porch.com)'s public pro directory. **One row
per contractor**, carrying **businessName, trade, phone, website, street, city, state, zip,
latitude/longitude, rating, reviewCount, startYear/yearsInBusiness, the pro's full declared services
list, their ZIP-level serviceArea, and any state licence records Porch holds** (number, type, issuing
board and the board's own lookup URL).

Filter by trade, state, city, star rating, review count, years in business or whether the pro has
claimed their listing — or paste profile URLs straight in. **No login. No cookies. No CAPTCHA
solving.** Porch never challenges a request; it only rate-limits, and the Actor handles that.

### How big is it

**785,653 pro profiles.** That is not a marketing figure — it is the exact count from walking all 32
of Porch's child sitemaps end to end on **2026-08-12** (31 × 25,000 + 10,653), with **zero duplicate
URLs** across the whole set.

Inside it: **131 trades**, **50 states + DC + Puerto Rico, Guam, the US Virgin Islands and the
Northern Mariana Islands**, and **20,197 distinct city+state combinations** (plus one non-geographic
`national` bucket holding 103 profiles that carry no state). The ten biggest trades,
counted over all 785,653 URLs:

| Trade | Profiles | | Trade | Profiles |
|---|---:|---|---|---:|
| General Contractors | 141,500 | | Remodeling Contractors | 34,510 |
| Electricians | 60,430 | | HVAC Contractors | 32,334 |
| Plumbers | 51,920 | | Roofers | 31,465 |
| Handymen | 36,368 | | Landscapers | 27,891 |
| Painters | 35,777 | | Garage Door Specialists | 17,777 |

Biggest states: **CA 121,855 · TX 81,180 · FL 64,665 · NC 30,450 · NY 29,932 · OH 26,662**.
Biggest cities: **Houston 8,486 · Los Angeles 6,498 · San Antonio 5,783 · Chicago 5,230 ·
Austin 4,997**. Every one of those counts is in the trade and state dropdowns, so you can size a run
before you pay for it.

### Quick start

Click **Try for free** and hit **Start**. This is the input the Actor ships with — no edits needed:

```json
{
  "trades": ["roofers"],
  "states": ["TX"],
  "maxItems": 50,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

That exact run, executed twice on 2026-08-12: **50 rows in 30 and 34 seconds**, 57 pages fetched
each time, 0 failures.

Start a run that names no **trade**, **state**, **city** or **profile URL** — an empty `{}` from an
API call or agent, say — and the Actor runs *that same sample* (50 roofers in Texas, **$0.18**) and
logs which inputs to set. It never buys a random nationwide slice on your behalf.

1. Pick one or more **Trades** from the dropdown (each shows its live profile count).
2. Pick one or more **States** (same — each shows its count). Or add **Cities** for a tighter slice.
3. Set **Max results** — that is your hard cost cap.
4. **Start.** Rows are pushed to the dataset in batches of 500 as they are collected; export CSV /
   Excel / JSON.
5. To page further next time, set **Skip** to the number of profiles the last run scanned (with no
   lead-quality filter on, that is the number of rows you bought).

### The wedge: the fields the profile page never prints

Every Porch profile server-renders its whole application state into the page. That blob — not the
visible HTML — is what this Actor reads, and it carries five things you cannot get by looking at the
page or by scraping Google Maps:

- **The FULL services list.** The page shows 5 and hides the rest behind "+ Show all services". One
  pro in the 399-profile sample declared **476 services**; the median pro who publishes any declares
  **40**. That is the list that tells you whether a roofer also does gutters and attic insulation.
- **The FULL postal service area.** Not "serves Austin" — the actual ZIP list. Median **46 ZIPs**,
  widest in the sample **872 ZIPs across 595 named cities**. This is the field that tells you whether
  a pro can actually take a job at a given address.
- **Latitude and longitude on 100% of rows**, alongside the street address (95.5%).
- **`startYear`**, so `yearsInBusiness` is computed rather than guessed (58.1% of profiles).
- **State licence records** with the issuing board's own lookup URL — see below.

#### Licence records: a lead to verify, not proof

Porch caches state licence records but **does not keep them current**. In the 399-profile sample,
**22 profiles carried a licence (5.5%), holding 25 records between them — and 24 of those 25 had
already expired.** Porch's cached status read `ACTIVE` on 23 of the 25, and 22 records were both
`ACTIVE` in the cache and already expired.

So the field is named **`statusOnRecord`**, never `status`, and every record ships an **`expired`
boolean computed against today's date**. What is genuinely useful is the number, the type, and the
authority — they tell you exactly which board to re-verify against, and each record carries that
board's lookup URL:

```json
{
  "number": "NAT-F158526-1",
  "type": "Lead Abatement (Firm - Certification)",
  "statusOnRecord": "ACTIVE",
  "expiresOn": "2020-11-05",
  "expired": true,
  "verifiedBySource": false,
  "authority": "Texas Department of State Health Services",
  "authorityUrl": "https://vo.ras.dshs.state.tx.us/datamart/selSearchTypeTXRAS.do?from=loginPage"
}
```

`verifiedBySource: true` means Porch got the record from the state registry rather than the pro's own
claim — that was true for only **4 of the 25** records in the sample. If you need licence data you can
act on, use our [California CSLB](https://apify.com/scrapersdelight/cslb-contractor-scraper) or
[Florida DBPR](https://apify.com/scrapersdelight/florida-dbpr-license-scraper) Actors, which read the
state registries directly, and use this Actor for the contact and coverage fields.

### Read this before you buy rows

Five things that would otherwise turn into a support ticket. All measured on **399 live profiles,
2026-08-12**, drawn from 8 of the 32 child sitemaps (47 states, 72 trades).

1. **Porch never publishes an e-mail address.** `email` is `null` on 100% of rows. `hasEmailOnFile`
   (79.4% true) tells you Porch holds one; the address itself is not on the page and nothing can
   produce it from this source. See "Honest limits" for what to do instead.
2. **Two kinds of listing share one schema.** 76.4% are **claimed** accounts a pro maintains; 23.6%
   are **stubs Porch built itself**. They are very different leads and the headline count hides it.
3. **Rating fill is inverted against claimed status.** The stubs carry a rating **76%** of the time;
   the claimed accounts only **19%**. So `claimedOnly` + a rating floor is a narrow, expensive
   combination — measured, it leaves **11.3% of the directory and fetches 8.9 pages for every row you
   keep**. Legitimate, but budget the time for it.
4. **Cached licence status is stale** — 24 of 25 records already expired. Read `expired`, not
   `statusOnRecord`.
5. **A small unfiltered run is not a representative sample of the directory.** Results follow Porch's
   own sitemap order, and the sitemaps are not shuffled: child sitemap 0 is 5.4% general contractors,
   child sitemap 31 is 36.1%. If you want a representative trade mix, **select the trades** rather
   than taking the first N rows.

### What you get — one row per pro

Real values, taken from the sample row further down (Clear Choice Roofing, Austin TX, captured
2026-08-12).

**Identity & trade**

| Field | Example | Notes |
|---|---|---|
| `profileUrl` | `https://pro.porch.com/austin-tx/roofers/clear-choice-roofing-1/pp` | Canonical Porch URL |
| `companyId` | `3295` | Porch's own id — the dedupe key |
| `businessName` | `Clear Choice Roofing` | |
| `trade` | `Roofing Contractor` | The pro's primary trade, singular |
| `tradeCategory` / `tradeSlug` | `Roofers` / `roofers` | The slug is the value in the Trades dropdown |
| `secondaryTrades` | `[]` | Present on 16.0% of profiles |

**Contact**

| Field | Example | Notes |
|---|---|---|
| `phone` | `(512) 712-4906` | Normalised to `(NNN) NNN-NNNN` |
| `website` | `http://www.clearchoiceroofingatx.com` | Bare hosts are normalised to a URL |
| `email` | `null` | **Always null. Porch publishes none.** |
| `hasEmailOnFile` | `true` | Porch holds one; it is not on the page |

**Address & geo**

| Field | Example | Notes |
|---|---|---|
| `street` / `street2` | `13377 Pond Springs Rd Ste 105` / `null` | `street2` is rare (2 of 399) |
| `city` / `state` / `zip` | `Austin` / `TX` / `78729` | 100% fill |
| `latitude` / `longitude` | `30.44978` / `-97.78359` | 100% fill, numbers not strings |

**Ratings & reviews**

| Field | Example | Notes |
|---|---|---|
| `rating` | `4.79` | **0–5 scale, 2 decimals.** `null` = Porch published none |
| `reviewCount` | `367` | Aggregated across all sources Porch pulls from |
| `reviewSourceCount` | `4` | How many review sites fed that aggregate |
| `porchReviewCount` | `12` | Reviews written **on Porch itself**. `0` is a real value, not missing |

**Credentials & status**

| Field | Example | Notes |
|---|---|---|
| `startYear` / `yearsInBusiness` | `1990` / `36` | Computed against the current year |
| `licenseNumbers` / `licenses` | `["NAT-F158526-1"]` / see above | Read `expired`, not `statusOnRecord` |
| `accountStatus` | `USER_ACCOUNT` | `NO_ACCOUNT` | `USER_ACCOUNT` | `VERIFIED_ACCOUNT` |
| `isClaimed` / `isVerified` | `true` / `false` | `isVerified` was true on 1.0% of the sample |
| `businessStatus` / `acceptingWork` | `OPEN` / `true` | 396 of 399 `OPEN`, 3 `UNKNOWN` |
| `isVetted` / `isNationalPro` | `false` / `false` | **Both were false on all 399.** See Honest limits |

**Coverage & content**

| Field | Example | Notes |
|---|---|---|
| `services` / `serviceCount` | `["Attic Insulation Installation", …]` / `17` | Count is returned even when the array is off |
| `serviceAreaCities` / `serviceAreaCityCount` | `["Adkins", "Atascosa", …]` / `64` | |
| `serviceAreaZips` / `serviceAreaZipCount` | `["73301", "78680", …]` / `235` | |
| `description` | `Family owned and operated and an Owens Corning…` | Self-written, 59.9% fill |
| `photoUrl` | `https://imagescdn.staticp.com/…` | Porch stock clip-art returns `null`, not a fake logo |
| `sourceUrl` / `scrapedAt` | sitemap URL / `2026-08-13T01:46:06.878Z` | `scrapedAt` is ISO-8601 UTC |

### Field fill — measured on 399 live profiles

A random national walk of **399 live profiles**, drawn from 8 of the 32 child sitemaps — **47 states,
72 trades**, captured **2026-08-12**. Sorted by fill, so the sparse fields are impossible to miss.

| Field | Fill | Note |
|---|---:|---|
| `businessName`, `trade`, `city`, `state`, `zip`, `latitude`/`longitude`, `businessStatus` | **100%** | |
| `acceptingWork` | 100% | Returned on every row; `true` on 99.7% of them |
| **`phone`** | **99.7%** | 1 of 399 profiles had none |
| `street` | 95.5% | |
| `hasEmailOnFile` (true) | 79.4% | Porch holds an address; it never prints it |
| `isClaimed` (true) | 76.4% | |
| `services` | 73.9% | The **full** list — the page itself truncates at 5 |
| `serviceAreaCities` / `serviceAreaZips` | 71.9% | Median 46 ZIPs, widest 872 |
| **`website`** | **65.2%** | The handle to enrich e-mail from |
| `description` | 59.9% | |
| `startYear` / `yearsInBusiness` | 58.1% | |
| **`rating` / `reviewCount`** | **32.1%** | Most of the directory is unrated |
| `photoUrl` | 25.8% | Stock Porch clip-art returns `null`, not a fake logo |
| `secondaryTrades` | 16.0% | |
| `licenseNumbers` / `licenses` | 5.5% | 22 profiles, 25 records, 24 expired |
| `isVerified` (true) | 1.0% | 4 of 399 |
| `isVetted` / `isNationalPro` (true) | 0.0% | Both false on all 399 |
| **`email`** | **0.0%** | Porch never publishes it. Nothing can change that |

**The headline that could mislead you: this is not a ratings product.** 67.9% of Porch profiles carry
no rating at all. It *is* a phone-and-address product — 99.7% and 95.5%.

#### Claimed vs unclaimed, split

76.4% are claimed accounts (`USER_ACCOUNT` 301 + `VERIFIED_ACCOUNT` 4), 23.6% are stubs Porch built
itself (`NO_ACCOUNT` 94). Same 399 profiles:

| | website | services | description | licence | **rating** | phone |
|---|---:|---:|---:|---:|---:|---:|
| `NO_ACCOUNT` — unclaimed stub, n=94 | 41% | **0%** | 56% | 4% | **76%** | 99% |
| Claimed account, n=305 | 73% | **97%** | 61% | 6% | **19%** | 100% |

Note the **inversion in the rating column**. The stubs are the ones Porch seeded from review
aggregators, so they carry a rating far more often than pros who claimed their page. Pick one:

- **`claimedOnly`** → a fuller record (services, website, a business that answers)
- **`minRating`** → social proof, mostly sitting on stub listings

#### The demo run, for contrast

One prefilled Texas-roofers run (n=50, 2026-08-12) measured **phone 100% · street 98% · website 92% ·
rating 64% · years 74% · services 62% · licence 8% · claimed 66%**; a second measured website 90%,
years 72%, licence 6%. Much richer than the national average, because a specific trade in a big state
skews to real businesses — and wobbly, because 50 rows is 50 rows. **Plan against the 399-row national
table, not this one.** Every run prints its own measured fill in the log when it finishes.

### How to run it

#### By trade and state (the default)

Trade, state and city are read off the profile URL **before any page is fetched**, so these three
filters cost you nothing — no wasted request, no wasted money.

```json
{ "trades": ["plumbers", "hvac-contractors"], "states": ["TX"], "maxItems": 2000 }
```

#### By city

```json
{ "trades": ["general-contractors"], "states": ["TX"], "cities": ["houston", "sugar-land", "katy"], "maxItems": 1000 }
```

Cities are the city part of the URL slug. `"Sugar Land"` works too — spaces are converted for you.

#### By pasted profile URLs

```json
{ "profileUrls": [
    "https://pro.porch.com/austin-tx/roofers/clear-choice-roofing-1/pp",
    "https://pro.porch.com/murphy-tx/plumbers/first-class-plumbing-157145801/pp"
] }
```

This skips the sitemap entirely — the fastest path, and the one to use for refreshing a list you
already own. It overrides the trade/state/city filters. A URL that is not a Porch pro-profile URL
stops the run with an error naming the bad one.

#### Lead-quality filters, and what they cost

These live on the profile page, so a page has to be fetched before a pro can be rejected. **You are
never billed for a filtered-out row** — it costs run time, not money. Measured pass rates on the 399
sample:

| Filter | Keeps | Pages fetched per row kept |
|---|---:|---:|
| `claimedOnly` | 76.4% | 1.3 |
| `withWebsiteOnly` | 65.2% | 1.5 |
| `minYearsInBusiness: 10` | 50.4% | 2.0 |
| `minRating: "4"` | 24.6% | 4.1 |
| `minReviewCount: 10` | 24.8% | 4.0 |
| `minRating: "4.5"` | 17.0% | 5.9 |
| `claimedOnly` + `minRating: "4"` | 11.3% | 8.9 |

#### Slimming the output

`includeServices: false` and `includeServiceArea: false` drop the two long arrays. `serviceCount`,
`serviceAreaCityCount` and `serviceAreaZipCount` are still returned, so you keep the signal without
the JSON. **This saves dataset size, not requests — the price is identical either way.**

#### Paging across runs

```json
{ "trades": ["roofers"], "states": ["TX"], "maxItems": 1000, "skip": 1000 }
```

Results follow sitemap order, which is stable, so `skip` continues where the last run stopped instead
of re-buying the same pros. **`skip` counts profiles scanned, not rows delivered.** With no
lead-quality filter on, those are the same number. If you switch one on, the run scans more profiles
than it keeps, so set `skip` to the **pages fetched** figure the previous run printed in its log —
otherwise the next run restarts inside ground you already paid for. Pair it with an **Apify Schedule** (Actor → Schedules → e.g. `0 6 * * 1`
for Monday 06:00 UTC) and raise `skip` by your `maxItems` each week to walk the directory.

### Sample row

One real record from the default run, captured 2026-08-12. Long arrays are truncated here with `…`
and are complete in the dataset.

```jsonc
{
  "profileUrl": "https://pro.porch.com/austin-tx/roofers/clear-choice-roofing-1/pp",
  "companyId": 3295,
  "businessName": "Clear Choice Roofing",

  "trade": "Roofing Contractor",
  "tradeCategory": "Roofers",
  "tradeSlug": "roofers",
  "secondaryTrades": [],

  "phone": "(512) 712-4906",
  "website": "http://www.clearchoiceroofingatx.com",
  "email": null,                       // always null — Porch publishes none
  "hasEmailOnFile": true,              // Porch holds one, it is just not on the page

  "street": "13377 Pond Springs Rd Ste 105",
  "street2": null,
  "city": "Austin",
  "state": "TX",
  "zip": "78729",
  "latitude": 30.44978,
  "longitude": -97.78359,

  "rating": 4.79,                      // 0-5 scale, not a percentage
  "reviewCount": 367,                  // aggregated across 4 sources
  "reviewSourceCount": 4,
  "porchReviewCount": 12,              // written on Porch itself

  "startYear": 1990,
  "yearsInBusiness": 36,

  "licenseNumbers": ["NAT-F158526-1"],
  "licenses": [
    {
      "number": "NAT-F158526-1",
      "type": "Lead Abatement (Firm - Certification)",
      "statusOnRecord": "ACTIVE",      // Porch's CACHED status
      "expiresOn": "2020-11-05",
      "expired": true,                 // computed today - this is the field to trust
      "verifiedBySource": false,
      "authority": "Texas Department of State Health Services",
      "authorityUrl": "https://vo.ras.dshs.state.tx.us/datamart/selSearchTypeTXRAS.do?from=loginPage"
    }
  ],

  "isVetted": false,
  "accountStatus": "USER_ACCOUNT",
  "isClaimed": true,
  "isVerified": false,
  "businessStatus": "OPEN",
  "acceptingWork": true,
  "isNationalPro": false,

  "services": ["Attic Insulation Installation", "Attic Ventilation Installation", "…"],
  "serviceCount": 17,
  "serviceAreaCities": ["Adkins", "Atascosa", "Austin", "Bastrop", "…"],
  "serviceAreaCityCount": 64,
  "serviceAreaZips": ["73301", "78680", "78682", "…"],
  "serviceAreaZipCount": 235,

  "description": "Family owned and operated and an Owens Corning 'Platinum Preferred Contractor'…",
  "photoUrl": "https://imagescdn.staticp.com/api/image/display/custom/Clear%20Choice%20Roofing/…",

  "sourceUrl": "https://pro.porch.com/austin-tx/roofers/clear-choice-roofing-1/pp",
  "scrapedAt": "2026-08-13T01:46:06.878Z"
}
```

Fields people misread:

- **`rating` is a 0–5 scale with two decimals**, not a percentage. `null` means Porch published no
  rating; there is no `0` rating in this data.
- **`porchReviewCount: 0` is a real value** — the pro has reviews aggregated from elsewhere but none
  written on Porch. `reviewCount: null` means no reviews anywhere.
- **`statusOnRecord` is Porch's cache, `expired` is the truth.** 22 of the 25 licence records in the
  sample said `ACTIVE` and had already expired (24 of 25 had expired; 23 of 25 still read `ACTIVE`).
- **`isVerified` is Porch's `VERIFIED_ACCOUNT` badge (1.0%)**; `isClaimed` is the much broader "this
  pro has an account" (76.4%). They are not the same thing.

### Input

Fields in the order they appear in the Console.

| Field | Type | Default | What it does |
|---|---|---|---|
| `trades` | multi-select, 131 options | `["roofers"]` (prefill) | Which trades to pull. Each option shows its live profile count. Free filter — read off the URL, no page fetched. Empty = all trades |
| `states` | multi-select, 55 options | `["TX"]` (prefill) | States, DC and territories, each with its profile count. Free filter. Empty = nationwide |
| `cities` | string list | empty | Porch city slugs (`houston`, `los-angeles`, `west-sacramento`). 20,197 exist, so this is paste-in rather than a dropdown. `"West Sacramento"` also works. Free filter |
| `profileUrls` | string list | empty | Paste exact profile URLs. Skips the sitemap; overrides trade/state/city. A non-Porch URL is named in the log and skipped |
| `claimedOnly` | boolean | `false` | Keep only claimed accounts (76.4%). Fuller records — but they carry a rating only 19% of the time |
| `withWebsiteOnly` | boolean | `false` | Keep only pros with a website (65.2%). The field to filter on before e-mail enrichment |
| `minRating` | select | `Any rating` | 3.0 / 3.5 / 4.0 / 4.5 / 5.0 floor. Only 32.1% carry any rating, so a floor discards ~two thirds |
| `minReviewCount` | number | `0` | Aggregated-review floor. Same caveat as the rating floor |
| `minYearsInBusiness` | number | `0` | Founding-year floor. Present on 58.1%; the rest are dropped when set |
| `maxItems` | number | `1000` (prefill `50`) | **Your hard cost cap.** 50 = $0.18, 1,000 = $3.50, 10,000 = $35. Clamped to 50 when the run names no trade, state, city or profile URL |
| `skip` | number | `0` | Skip this many matching profiles first — page across runs without re-buying rows |
| `includeServices` | boolean | `true` | Include the full services array. Off = smaller dataset, **same price, same requests** |
| `includeServiceArea` | boolean | `true` | Include the coverage cities + ZIPs. Off = smaller dataset, **same price, same requests** |
| `maxConcurrency` | number | `8` | Parallel profile fetches. 8 is measured-optimal; higher just earns HTTP 429s |
| `proxyConfiguration` | proxy | Apify Proxy (automatic) | Leave it on. Datacenter is enough — Porch is not bot-walled |

#### Trade slugs

All 131 are in the dropdown with their profile counts. The twenty biggest, if you are building input
by API:

`general-contractors` 141,500 · `electricians` 60,430 · `plumbers` 51,920 · `handymen` 36,368 ·
`painters` 35,777 · `remodeling-contractors` 34,510 · `hvac-contractors` 32,334 · `roofers` 31,465 ·
`landscapers` 27,891 · `garage-door-specialists` 17,777 · `home-cleaners` 16,432 ·
`flooring-contractors` 14,424 · `pest-control-contractors` 12,110 · `locksmiths` 11,056 ·
`appliance-repair-services` 10,771 · `window-contractors` 10,635 · `real-estate-professionals` 10,492 ·
`carpet-cleaners` 9,251 · `tree-contractors` 9,102 · `home-inspectors` 8,776

The long tail runs down to `soil-engineers` (69), `decorative-hardware-retailers` (57),
`earthquake-retrofitting-specialists` (47) and `storm-shelter-contractors` (33).

### Pricing

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

| Run | Rows | Cost |
|---|---:|---:|
| Default smoke test (TX roofers) | 50 | $0.18 |
| Every pro in Houston | 8,486 | $29.70 |
| Every roofer in the country | 31,465 | $110.13 |
| Every general contractor in the country | 141,500 | $495.25 |
| The entire Porch pro directory | 785,653 | $2,749.79 |

- **You are charged for rows delivered, never for a page that was fetched and then filtered out.**
  The default run fetched 57 pages and delivered 50; you pay for 50.
- **The source itself has no repeats** — 785,653 sitemap URLs, 0 duplicates (see Uniqueness), so a
  sitemap-driven run cannot bill you twice for the same pro. The one way to double-pay is to paste
  the same URL twice into `profileUrls`; the Actor takes that list literally.
- **Rows are charged as they are pushed** (`Actor.pushData(items, 'pro-scraped')`), so if you hit a
  budget cap you get whole rows and stop — never a half-billed dataset.
- **`maxItems` is your cost dial**, and it is enforced before a row is counted, so parallel workers
  cannot overshoot it.

### Honest limits

- **No e-mail addresses. None, for anybody.** `email` is null on 100% of rows. `hasEmailOnFile` is
  true for 79.4% of pros, which tells you Porch has one on file and is not showing it. Anyone selling
  you "Porch e-mails" generated them somewhere else. **What to do instead:** take the 65.2% of rows
  that carry a `website` and run them through
  [Decision-Maker Email Finder](https://apify.com/scrapersdelight/decision-maker-email-finder), which
  turns a domain into named contacts with pattern-detected, MX-validated addresses.
- **Only 32.1% of profiles carry a rating**, and `reviewCount` fill is the same 32.1%. A rating floor
  is a legitimate filter, but it discards roughly two thirds of the directory and costs 4.1 pages per
  row kept at 4.0+. This is what Porch published, not a scraping failure.
- **Cached licence status is stale — 24 of the 25 records in the sample had expired**, and only 4 of
  25 came from a state registry (`verifiedBySource`). Use `expired`, and re-verify at
  `authorityUrl`. For licence data you can act on, use the
  [CSLB](https://apify.com/scrapersdelight/cslb-contractor-scraper) or
  [DBPR](https://apify.com/scrapersdelight/florida-dbpr-license-scraper) Actors.
- **`isVetted` and `isNationalPro` were `false` on all 399 sampled profiles.** They are real fields in
  Porch's payload and they are returned honestly, but do not build a filter on them expecting volume.
- **`street` is the pro's business address, not their service area.** A Houston-registered plumber may
  serve 200 ZIPs; that is what `serviceAreaZips` is for. Filtering by `cities` filters on the
  registered address.
- **US only.** Porch's pro directory covers the 50 states, DC and four territories. There is no
  Canadian or UK data here — for Canada use
  [HomeStars](https://apify.com/scrapersdelight/homestars-scraper).
- **Sitemap order is Porch's order, not a quality ranking**, and it is not shuffled — child sitemap 0
  is 5.4% general contractors and child sitemap 31 is 36.1%. Select trades explicitly if you want a
  representative mix.
- **No login, no CAPTCHA solving, no browser automation.** Everything here comes from pages Porch
  serves anonymously and lists in its own public sitemap. There is no hidden mode that returns more.

### How it works, and the transport ladder

1. Read `https://pro.porch.com/sitemap-pro-profile-index.xml` → 32 child sitemaps → 785,653 profile
   URLs. Every URL is `/{city}-{st}/{trade-slug}/{business}/pp`, so **state, city and trade are known
   before a single page is fetched** — that is why those filters are free.
2. Fetch each matching profile page and read the server-rendered application state out of it. That
   blob holds the fields the visible page never prints. JSON-LD is present too but only carries
   name/phone/city — and its `addressRegion` is actually the ZIP — so it is used strictly as a
   fallback.
3. No login, no cookie, no API key, no browser. The parser keys off the JSON payload, never off CSS
   class names (Porch's are hashed and change on every deploy).

**The wall here is a rate limit, not a bot check.** Porch throttles per exit IP with a short-window
token bucket that returns HTTP 429, and never challenges. Measured **through Apify** on 2026-08-12
with retries switched off:

| Rung | Result |
|---|---|
| Direct, concurrency 8, 40 URLs @ 11.2 req/s | 40/40 = **100%** |
| Direct, concurrency 12, 200 URLs @ 31.1 req/s | 68/200 = **34.0%** (132 × HTTP 429) |
| Direct, concurrency 20, bucket already drained | 0/199 = **0%** (199 × HTTP 429) |
| Apify Proxy, one pinned session, concurrency 8 | 40/40 = **100%** @ 5.97 req/s |
| **Apify Proxy, fresh session per request, concurrency 8** | 59/60 = **98.3%** (the miss was a proxy `595 ECONN`) |

So the Actor defaults to **Apify Proxy with a new session minted for every request** — each one
starts on a full token bucket — and retries a failure on another fresh exit IP with exponential
backoff. With that loop in place, the validation runs fetched **556/556**, then **399/399**, then
**57/57** and **57/57** profile pages at concurrency 8 with **zero failures** — 1,069 consecutive
successful content requests. Because Porch is not bot-walled, the cheap datacenter rung is enough: **no residential
bandwidth is needed and none is used.**

### Uniqueness

Walked **all 32 child sitemaps contiguously, end to end**: 785,653 URLs, **0 duplicates**. Porch's
sitemap is a clean enumeration — one URL per pro, no promoted repeats, no paging overlap.

That is worth stating plainly rather than dressing up: **there is no dedupe pass in this Actor,
because the source does not need one.** Both live runs confirmed it downstream — **399 rows → 399
distinct `companyId`s**, and **50 rows → 50 distinct `companyId`s**. The one exception is
`profileUrls`: that list is taken literally, so if you paste a URL twice you get (and pay for) two
rows.

Across runs, use `skip`. Sitemap order is stable, so `skip: 1000` after an unfiltered
`maxItems: 1000` run resumes exactly where you stopped — with a lead-quality filter on, skip by the
run's **pages fetched** count instead (see *Paging across runs*).

### When a run fails

This Actor never hands you a green run with an empty dataset and no explanation. Every stop is
named — in the log and in the run's status message — and **nothing that was not delivered is
billed**:

- **0 rows emitted** → stops with the counts behind it: pages fetched, network failures, dead
  profiles, unparseable pages and how many rows your filters removed — so you can tell a filter
  problem from a site change. Nothing charged.
- **Sitemap index shrinks below 20 children** (the live count is 32) → stops rather than shipping a
  partial slice of the directory. Nothing charged.
- **Every child sitemap comes back empty or unreachable** → stops; treat it as a format change at
  Porch. Nothing charged.
- **No candidate matched your filters** → stops with the exact scope it scanned and the correct
  spelling conventions for states, cities and trades.
- **A bad `profileUrls` entry** → named in the log and skipped; the valid URLs still run. All of
  them bad → stops without fetching anything.
- **Run time runs out** (a short run timeout on a big `maxItems`) → stops *fetching* with time in
  hand, **pushes everything already collected**, and tells you to raise the timeout or continue with
  `skip`. A partial run is delivered and billed for what it delivered, never lost.
- **More than 20% of pages return HTTP 200 with no parseable state** → this one **fails the run
  outright**. It is the single case where a green run would be actively misleading: the pages
  loaded, so a silent success would look like real coverage of a thin extraction.

There is no "legitimate zero" mode here. A successful run always has rows.

### Who buys this

- **Building-materials and equipment distributors** — `trade` + `serviceAreaZips` + `phone` gives you
  every roofer who actually covers a target ZIP, not just the ones headquartered in it.
- **Franchise and roll-up acquirers in home services** — `startYear`, `serviceCount` and
  `reviewCount` are the screen; 141,500 general contractors is the funnel.
- **SaaS selling into trades (field-service, scheduling, invoicing)** — the 76.4% claimed accounts are
  businesses that already maintain an online presence, which is the ICP; the 34.8% without a website
  are the ICP for anyone selling websites.
- **Insurance and bonding brokers** — `licenses[].expired` plus `authorityUrl` is a renewal-outreach
  list with the verification path attached.
- **Lead-gen and home-warranty marketplaces** — `serviceAreaZips` + `acceptingWork` tells you who can
  take a job in a postcode today.
- **Local-SEO and reputation agencies** — the 25.8% with no photo and the 67.9% with no rating are a
  ready-made pitch list.

### Sibling Actors

| Actor | What it is | Why you would use it instead |
|---|---|---|
| [Thumbtack Scraper](https://apify.com/scrapersdelight/thumbtack-scraper) | Top home-service pros by category + US city | Hires count, Top Pro badge and response time — signals Porch does not carry (no phone or e-mail on that surface) |
| [Houzz Professionals Scraper](https://apify.com/scrapersdelight/houzz-pro-scraper) | Houzz pros by trade and city | Design/remodel end of the market: budget band, Best of Houzz badges, social links |
| [BBB Scraper](https://apify.com/scrapersdelight/bbb-scraper) | Better Business Bureau search results | When you want the BBB letter grade and accreditation status as the quality screen |
| [California CSLB Scraper](https://apify.com/scrapersdelight/cslb-contractor-scraper) | All 243,555 licensed CA contractors, official registry | Licence data you can act on: current status, surety bond, workers' comp carrier |
| [Florida DBPR Scraper](https://apify.com/scrapersdelight/florida-dbpr-license-scraper) | Licensed FL construction contractors, official records | Same, for Florida |
| [HomeStars Scraper](https://apify.com/scrapersdelight/homestars-scraper) | Canadian contractor leads | Porch is US-only |
| [Decision-Maker Email Finder](https://apify.com/scrapersdelight/decision-maker-email-finder) | Domain → named contacts + validated e-mail | The other half of a Porch list: Porch gives you the website, this gives you the inbox |

Porch is where you go for **phone, street address, geo and coverage at national scale**. CSLB and
DBPR are where you go for **licence truth**. Neither replaces the other.

### FAQ

**Does this need an account or login?**
No. No login, no cookie, no API key, no CAPTCHA solving service. Every page read is one Porch serves
anonymously and lists in its own public sitemap.

**Do I get e-mail addresses?**
No — Porch publishes none, for anyone. 79.4% of pros have `hasEmailOnFile: true`, which only tells you
Porch holds one. Use the 65.2% of rows with a `website` and enrich from there.

**Can I get the whole directory in one run?**
Yes — select all 55 states (or all 131 trades) and set `maxItems: 785653`, about $2,749.79 at
$0.0035/row. Naming a scope is what unlocks it: a run that names no trade, state, city or profile
URL is treated as an unconfigured first click and runs the 50-row Texas-roofers sample instead, so
nobody buys a nationwide slice by accident. Most buyers slice by trade and state anyway, which is
free to do because those are read off the URL.

**Do I get charged for rows my filters removed?**
No. You are billed per pro delivered. The default run fetched 57 pages and delivered 50, and you pay
for 50. A `minRating: "4"` run fetches about 4.1 pages for every row you keep — that costs time and
the Actor's proxy budget, not your money.

**Two runs — will I get duplicates?**
Not from the sitemap: it holds 785,653 URLs with zero duplicates, verified by walking all 32 child
sitemaps end to end, so a run cannot return the same pro twice. Across runs, set `skip` to the number
of rows you already have; sitemap order is stable.

**Is the address the pro's office or the area they serve?**
`street`/`city`/`state`/`zip` is the registered business address. Coverage is a separate field —
`serviceAreaZips`, median 46 ZIPs and up to 872. Filtering by `cities` filters on the address.

**Does it need a residential proxy?**
No, and that is a cost win. Porch is not bot-walled — it only rate-limits per IP. The default Apify
Proxy (automatic, datacenter) measured 100% at concurrency 8, and the last three validation runs went
1,069 for 1,069 requests.

**Why does my small run look nothing like the fill table?**
Because a filtered slice is richer than the national average, and because sitemap order is not
shuffled. The prefilled TX-roofers run measured 92% website fill against a national 65.2%. Plan
against the 399-row national table.

**Are the licence records safe to rely on?**
No — treat them as a lead. 24 of the 25 records in the sample had already expired, and 22 of those
still read `ACTIVE` in Porch's cache. Read the `expired` boolean and re-verify at `authorityUrl`.

**How do I pull a specific list of pros I already know?**
Paste their profile URLs into `profileUrls`. That skips the sitemap and is the fastest way to refresh
an existing list.

**Can I schedule it?**
Yes. Save your input as a Task, then attach an Apify Schedule (e.g. `0 6 * * 1` for Mondays at 06:00
UTC) and raise `skip` by your `maxItems` each week to walk the directory over time.

**Will a run ever succeed with zero rows?**
No. A zero-row run throws with the full scope and HTTP counts. There is no quiet-empty mode.

**Something looks wrong — how do I debug it?**
The run log prints the sitemap count, the candidate count, per-batch progress and a final measured
fill line for that exact run. Compare it against the table above; if the fill collapsed, the run will
usually have thrown already.

### Legal & fair use

This Actor reads public Porch.com pro profile pages that Porch itself publishes and enumerates in its
own `sitemap-pro-profile-index.xml`. It does not log in, does not solve CAPTCHAs, does not use
browser automation, and collects nothing behind any authentication. It requests only URLs Porch lists
for crawlers.

Records describe **businesses**, but a sole trader's business phone and address can be personal data.
You are responsible for complying with Porch.com's terms and with how you use the data — including
CAN-SPAM, TCPA and state do-not-call rules for outreach, and CCPA/CPRA and GDPR where they apply to
the people behind these listings.

Porch® is a trademark of its owner. This Actor is not affiliated with, endorsed by, or sponsored by
Porch.com.

### Feedback

Found a missing field, a trade that should be in the dropdown, or want a new filter? Open an issue on
the **Issues** tab — it is read.

# Actor input Schema

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

Which Porch trades to pull. The number after each name is how many profiles that trade holds in the live directory, counted across all 785,653 sitemap URLs on 2026-08-12 — so you can size a run before you start it. Leave empty for every trade. Example: General Contractors (141,500) plus Electricians (60,430).

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

US states, DC and territories, each with its live profile count (counted across all 785,653 sitemap URLs on 2026-08-12). Leave empty for nationwide. Filtering here is free — the state is in the URL, so no page is ever fetched for a pro outside your selection.

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

Porch city slugs — the city part of the profile URL. Porch publishes 20,197 city+state combinations, far too many for a dropdown, so this is a paste-in list: houston, los-angeles, san-antonio, new-york, west-sacramento. Spaces are converted for you, so "West Sacramento" also works. Biggest by profile count: houston 8,486 · los-angeles 6,498 · san-antonio 5,783 · chicago 5,230 · austin 4,997. Leave empty for every city in the selected states.

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

Paste exact Porch profile URLs (https://pro.porch.com/{city}-{st}/{trade}/{business}/pp) to re-scrape or refresh a list you already have. This path skips the sitemap entirely, so it is the fastest way to run, and it overrides the trade / state / city filters. A URL that is not a Porch pro-profile URL stops the run with an error naming the bad one, rather than quietly returning nothing.

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

Keep only pros who have claimed and maintain their Porch listing (a live account rather than a directory stub Porch built itself). Measured 76.4% of profiles, so about 1.3 pages are fetched per row kept. Claimed pros carry a service list 97% of the time vs 0% for stubs, and a website 73% vs 41% — but a rating only 19% of the time vs 76%, so combining this with a rating floor gets expensive fast.

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

Keep only pros whose profile lists a company website (measured 65.4% of profiles). This is the field to filter on when you plan to enrich for e-mail, because Porch itself never publishes an e-mail address.

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

Drop pros rated below this. Only 32.1% of Porch profiles carry any rating at all and a floor drops every unrated pro, so 4.0+ keeps 24.6% of the directory and fetches 4.1 pages for every row you keep. Ratings also sit mostly on UNCLAIMED listings (76% of stubs vs 19% of claimed accounts), so combining 4.0+ with "Only claimed listings" keeps just 11.3% at 8.9 pages per row. "Any rating" keeps everyone.

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

Drop pros with fewer aggregated reviews than this. Same caveat as the rating floor — the 67.9% of pros Porch shows no reviews for are dropped as well. 0 = keep everyone.

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

Drop pros founded more recently than this. Computed from Porch's founding year, which is present on 58.1% of profiles; the other 41.9% are dropped whenever a floor is set. 0 = keep everyone.

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

How many pro records to return, and your cost cap: 50 rows = $0.18, 1,000 = $3.50, 10,000 = $35. One record is one profile page fetched, at roughly 5 pros/second (measured: 399 pages in 87 seconds at the default concurrency). The prefilled 50 is a smoke test — raise it once you like the shape of the rows. Start a run without picking any trade, state, city or profile URL and the Actor runs the documented sample instead — 50 roofers in Texas, $0.18 — rather than charging you for a random nationwide slice.

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

Skip this many matching profiles before collecting. Results follow sitemap order, which is stable, so a maxItems=1000 run followed by skip=1000 continues where the first stopped instead of re-buying the same pros. It counts profiles SCANNED, not rows delivered: with no lead-quality filter on they are the same number, but if you use one, set skip to the "pages fetched" figure the previous run printed in its log or the next run will start inside ground you already paid for. All 785,653 sitemap URLs are unique — verified by walking all 32 child sitemaps end to end — so paging this way never returns a duplicate.

## `includeServices` (type: `boolean`):

Include the pro's full declared service list — the profile page itself truncates it at 5 behind "+ Show all services", this returns all of them. One pro in the 399-profile sample listed 476. Turn it off for a slimmer spreadsheet; serviceCount is returned either way and the price does not change.

## `includeServiceArea` (type: `boolean`):

Include the pro's coverage cities and ZIP codes. The median pro who publishes one covers 46 ZIPs; the widest in the 399-profile sample reached 872. Turn it off for a slimmer spreadsheet; serviceAreaCityCount and serviceAreaZipCount are returned either way and the price does not change.

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

Parallel profile fetches. 8 is the tested default: live runs pulled 399 of 399 and 556 of 556 profile pages with zero failures at that setting. Higher values gain little because Porch rate-limits per exit IP — direct at concurrency 12 collapsed to 34.0% (132 HTTP 429s out of 200 requests).

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

Leave it on Apify Proxy (automatic) — that is the measured-best rung and it needs no residential bandwidth. Porch never challenges you (no Cloudflare, no CAPTCHA, no cookie, no login); it only throttles per exit IP, so the Actor mints a fresh proxy session for every request and each one starts on a full token bucket. Switching the proxy off works for short jobs but collapses to HTTP 429 above roughly 10 requests/second from a single address.

## Actor input object example

```json
{
  "trades": [
    "roofers"
  ],
  "states": [
    "TX"
  ],
  "claimedOnly": false,
  "withWebsiteOnly": false,
  "minRating": "0",
  "minReviewCount": 0,
  "minYearsInBusiness": 0,
  "maxItems": 50,
  "skip": 0,
  "includeServices": true,
  "includeServiceArea": true,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per Porch pro: business name, trade, phone, website, street address, lat/lon, rating, review count, years in business, services, service area and licence records.

# 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 = {
    "trades": [
        "roofers"
    ],
    "states": [
        "TX"
    ],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/porch-pro-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 = {
    "trades": ["roofers"],
    "states": ["TX"],
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/porch-pro-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 '{
  "trades": [
    "roofers"
  ],
  "states": [
    "TX"
  ],
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/porch-pro-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/porch-pro-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/cydDFWLA5QgRNcIvn/builds/sH52i4S4zHd3u0FG5/openapi.json
