# Angi Scraper — Contractor Leads by Trade & City (`scrapersdelight/angi-scraper`) Actor

Scrape home-services contractors from Angi (formerly Angie's List) across all 313 published trades and any US city: business name, phone, website, full address, rating, review count, star breakdown, years in business, licence numbers and the jobs they take. No login.

- **URL**: https://apify.com/scrapersdelight/angi-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Automation, Business
- **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

## Angi Scraper — Contractor Leads by Trade & City

Turn [Angi](https://www.angi.com) (formerly Angie's List) into a home-services contractor list you can call. One row is one contractor, carrying **name, phone, website, street, city, state, zip, serviceArea, averageRating, reviewCount, recommendedRate, starCounts, yearsInBusiness, licenses, tasksOffered, trades, acceptedPayments and isAdvertiser**. Pick the trade from Angi's own 313-category list, then name a city (`Austin, TX`) or a whole state (`TX`) — or just paste Angi URLs. No login, no API key, no CAPTCHA solving.

**313 published trades, measured live from Angi's geo sitemap index on 2026-08-12 — not quoted from a marketing page.** Angi publishes 20,115 city pages for plumbing alone, across 51 states; Austin, TX by itself lists 880 plumbers, Chicago 729 roofers, Phoenix 676 electricians.

The one number that decides whether this Actor is useful to you: on a 200-contractor walk of Austin plumbing, **phone was published for 99.1% of the 106 advertisers and 0.0% of the 94 non-advertisers.** Angi only prints a phone number for pros who pay it. This Actor tells you which is which on every row, and can bill you for only the contactable ones.

```json
{
  "mode": "search",
  "category": "plumbing",
  "locations": ["Austin, TX"],
  "includeContactInfo": true,
  "maxItems": 25
}
```

Click **Try for free** and press **Start** — that block is literally the input this Actor ships with. The verification run for this page (platform run `vRy621xTjxcWvRWvV`, 2026-08-15) took **78 seconds and returned 25 rows at 100.0% phone fill** for about nine cents.

### The wedge: `isAdvertiser` is a contactability oracle

Every home-services scraper on the store will hand you names and star ratings. Google Maps will too. What none of them hands you is **which contractors pay Angi, and what that predicts.**

Measured on a contiguous walk of Austin plumbing, pages 1 to 20, 200 contractors, 2026-08-12:

| | Advertisers (106) | Non-advertisers (94) |
|---|---|---|
| `phone` | **99.1%** | **0.0%** |
| `website` | 61.3% | 0.0% |
| `licenses` | 34.9% | 16.0% |
| `insured` true | 9.4% | 0.0% |
| `bestReviewText` | 67.0% | 91.5% |
| `starCounts` | 81.1% | 100.0% |

`isAdvertiser`, `isSponsored` and `isPaidPro` agreed on all 200 rows: 106 true, 94 false. That single boolean sorts an Angi city into the half you can phone today and the half you can only research — and because `onlyWithPhone` drops uncontactable rows **before** they are written or billed, you can buy just the first half. On this 200-row walk that turns a $0.70 run into a $0.37 run.

The second half of the wedge is coverage: the trade picker carries **all 313 categories Angi publishes**, each labelled with Angi's own wording, and a location field that accepts a whole state, not just one city.

### Read this before you buy rows

1. **Angi ranks advertisers first, so a small run is 100% advertisers.** On Austin plumbing, pages 1–10 were 10/10 advertisers, page 11 was 6/10, and pages 12–20 were 0/10. A 25-row or 50-row run is a list of paying pros — great for phone fill, useless as a census. If you want the whole market, raise `maxItems` and expect the phone column to thin out as you go deeper.
2. **The address is the contractor's own HQ, not the city you searched.** Angi lists pros who *serve* your city. On the 200-pro Austin search, only 31.0% were headquartered in Austin; the rest sat in 42 other towns — Leander, Round Rock, Georgetown, Cedar Park, Hutto. Use `serviceArea` for coverage and `street`/`city`/`state`/`zip` for where the business actually sits.
3. **There are no email addresses, anywhere.** Angi does not publish contractor emails on these pages. There is no `email` field and this Actor performs no guessing or enrichment.
4. **Two rows can be the same company under different trades.** A plumber appears under `plumbing`, `water-heaters` and `drain-cleaning`. Deduplication is per run, keyed on `spid`; running three trades in three runs can return the same business three times.
5. **`0` is a real value and it is not `null`.** `reviewCount: 0` means Angi shows no reviews (19 of the 200 rows); `averageRating: 0` came with it. `responseRate` is a 0–100 integer that was `0` on 44.0% of rows and `100` on 9.5% — do not sort a lead list on it without checking.

### What you get

One row per contractor. All dates are ISO-8601 UTC. `averageRating` is a 0–5 number; `recommendedRate` and `responseRate` are 0–100 integers — do not mix the scales.

**Identity & provenance**

| Field | Example | Notes |
|---|---|---|
| `spid` | `"159152389"` | Angi's stable service-provider id. **This is the dedupe key** — not the number in the URL, which is a placeholder `1` on many unclaimed listings. |
| `proUuid` | `"c8b4fab2-1c40-11ee-b010-12f6eb045c11"` | Angi's internal UUID. |
| `name` | `"Excalibur Plumbing"` | Business name. |
| `profileUrl` | `".../excalibur-plumbing-reviews-6136084.htm"` | The pro's own Angi page. |
| `category` | `"Plumber"` | Angi's noun for the searched trade. |
| `businessType` | `"LEADS"` | Was `"LEADS"` on all 200 rows measured. |
| `searchCategory` / `searchCity` / `searchState` / `sourceUrl` / `scrapedAt` | `"plumbing"`, `"austin"`, `"TX"`, … | What you asked for and when it was read. |

**Contact & address**

| Field | Example | Notes |
|---|---|---|
| `phone` | `"5122597222"` | Digits, no formatting. Profile fetch only. |
| `website` | `"http://www.excaliburplumbing.com"` | The pro's own site, never an angi.com or homeadvisor.com link. Profile fetch only. |
| `street` / `street2` | `"11880 Old 2243 W"` / `null` | |
| `city` / `state` / `zip` / `country` | `"Leander"` / `"TX"` / `"78641"` / `"US"` | The HQ, not the searched city. |
| `serviceArea` | `"Serving Austin, TX and surrounding areas"` | Angi's own coverage sentence. |

**Reputation**

| Field | Example | Notes |
|---|---|---|
| `averageRating` | `4.787` | 0–5, three decimals. |
| `reviewCount` | `2050` | Ranged 0–2,050 across the sample. |
| `recommendedRate` | `92` | 0–100. |
| `ratingBreakdown` | `{overall, quality, value, professionalism, punctuality, responsiveness}` | Each 0–5. |
| `starCounts` | `{star1:25, star2:24, star3:69, star4:127, star5:1803}` | Profile fetch only. |
| `bestReviewText` / `bestReviewDate` | `"Excalibur plumbing was just perfect to work with…"` | Angi's featured review. |
| `reviews[]` | author, rating, text, datePublished, cost, verified, wouldHireAgain, categories | Only when **Include recent reviews** is on. |

**Qualification & firmographics**

| Field | Example | Notes |
|---|---|---|
| `isAdvertiser` / `isSponsored` / `isPaidPro` | `true` | Who is paying Angi. Agreed on all 200 rows. |
| `isSuperServiceAwardWinner` / `isCorporateAccount` | `false` | |
| `yearsInBusiness` / `inBusinessSince` | `16` / `2010` | |
| `responseRate` | `5` | 0–100 integer; see gotcha 5. |
| `licensed` / `licenses[]` | `true` / `["38879"]` | Licence numbers as Angi prints them. Profile fetch only. |
| `bonded` / `insured` | `false` / `false` | Profile fetch only. See Honest limits. |
| `tasksOffered[]` | `["Water Heater - Install or Replace", …]` | From the listing card. |
| `trades[]` | `["Water Heater Installation and Replacement", …]` | From the profile. |
| `emergencyServices`, `freeEstimates`, `warrantiesOffered`, `veteranOwned`, `bilingual`, `smallJobsWelcome`, `offersCommercialServices`, `ecoFriendly` | booleans | Amenity flags. |
| `acceptedPayments[]` | `["CreditCard"]` | |
| `description` | `"Top rated plumbing company serving the Austin/Wilco area!…"` | The pro's own About-us text. |
| `logoUrl` | `"https://cdn.homeadvisor.com/…png"` | |

### Field fill — measured, twice, because the two runs differ

Every run also prints its own counts in the log. Below are two real samples, both taken 2026-08-12, and **they disagree — read the one that matches the run you intend to buy.**

#### A. What a default-shaped run returns — n = 150

The first 50 pros of each of three searches: plumbing in Austin TX, roofing in Chicago IL, electrical in Phoenix AZ. All 150 were advertisers, because that is how Angi ranks.

| Field | Fill |
|---|---|
| name, street, city, state, zip, serviceArea, description, category | 100.0% |
| **phone** | **100.0%** |
| averageRating, reviewCount, recommendedRate, ratingBreakdown, starCounts | 100.0% |
| inBusinessSince, responseRate, tasksOffered, trades, spid, profileUrl | 100.0% |
| yearsInBusiness | 98.0% |
| logoUrl | 91.3% |
| bestReviewText / bestReviewDate | 88.7% |
| acceptedPayments | 83.3% |
| **website** | **70.0%** (80.0% Austin plumbing, 70.0% Chicago roofing, 60.0% Phoenix electrical) |
| **licenses** | **65.3%** (50.0% / 70.0% / 76.0%) |
| street2 | 7.3% |

#### B. What a deep, whole-market run returns — n = 200

A contiguous walk of Austin plumbing pages 1–20. 106 advertisers, 94 not.

| Field | Fill |
|---|---|
| name, street, state, zip, serviceArea, description, spid, profileUrl, businessType | 100.0% |
| averageRating, reviewCount, recommendedRate, ratingBreakdown, inBusinessSince, responseRate | 100.0% |
| tasksOffered, trades | 100.0% |
| city | 99.0% |
| yearsInBusiness | 91.0% |
| starCounts | 90.0% |
| reviews (≥1 attached) | 90.0% |
| acceptedPayments | 82.0% |
| bestReviewText | 78.5% |
| logoUrl | 78.5% |
| **phone** | **52.5%** |
| **website** | **32.5%** |
| **licenses** | **26.0%** |
| street2 | 7.5% |

**The headline that could mislead you: phone fill is 100% on a small run and 52.5% on a deep one, and that is not variance — it is Angi's ranking.** Plan against table B if you intend to buy a whole city.

Boolean flags, share `true` on the same 200 rows: `freeEstimates` 71.0%, `isAdvertiser`/`isSponsored`/`isPaidPro` 53.0%, `warrantiesOffered` 46.5%, `offersCommercialServices` 42.5%, `emergencyServices` 38.5%, `bilingual` 29.5%, `smallJobsWelcome` 28.5%, `licensed` 26.0%, `isSuperServiceAwardWinner` 8.5%, `isCorporateAccount` 6.5%, `insured` 5.0%, **`bonded` 0.0%, `veteranOwned` 0.0%, `ecoFriendly` 0.0%.** None of them was ever `null`.

#### C. Listing-only mode — n = 200, same contractors, `includeContactInfo: false`

| Field | Fill |
|---|---|
| name, state, zip, serviceArea, description, ratings, recommendedRate, tasksOffered, spid | 100.0% |
| street, city | 99.0% |
| yearsInBusiness | 91.0% |
| logoUrl | 78.5% |
| bestReviewText | 78.0% |
| **phone, website, starCounts, trades, licenses, licensed, isPaidPro, inBusinessSince, acceptedPayments** | **0.0%** |

Same 200 contractors, **23 requests instead of 273 and 1m40s instead of 6m36s** (platform runs `kFDg8nMgWTqFFpzPu` vs `Kcy7Ovs8Ka2mT5Wzh`, 2026-08-15) — and the `isAdvertiser` flag survives at 53.0%, so you can still tell the two halves apart. Use it to map a market cheaply, then re-run with profiles on the slice you care about. The price per contractor is identical either way.

### How to run it

**Search by trade + one city** — the default.

```json
{ "mode": "search", "category": "roofing", "locations": ["Chicago, IL"], "maxItems": 100 }
```

**Search a whole state.** A bare state code sweeps every city Angi publishes for that trade in it. Always cap this.

```json
{ "mode": "search", "category": "hvac", "locations": ["TX"], "maxItems": 500 }
```

**Several cities in one run.** Each location is crawled with the same trade, so this is three city crawls.

```json
{ "mode": "search", "category": "electrical", "locations": ["Austin, TX", "Dallas, TX", "Houston, TX"], "maxItems": 300 }
```

**Paste Angi URLs.** Listing pages are paginated for you; profile pages are read directly. The Actor classifies each URL itself and fetches a repeated URL once.

```json
{
  "mode": "startUrls",
  "startUrls": [
    { "url": "https://www.angi.com/companylist/us/tx/austin/plumbing.htm" },
    { "url": "https://www.angi.com/companylist/us/tx/leander/excalibur-plumbing-reviews-6136084.htm" }
  ],
  "maxItems": 50
}
```

**Callable rows only, cheap.** Uncontactable rows are dropped before billing and the crawl keeps going to fill your quota.

```json
{ "mode": "search", "category": "plumbing", "locations": ["Austin, TX"], "onlyWithPhone": true, "maxItems": 100 }
```

**Map a market for the price of the rows, not the time.**

```json
{ "mode": "search", "category": "landscaping", "locations": ["AZ"], "includeContactInfo": false, "maxItems": 1000 }
```

### Sample row

A real row from the 200-contractor Austin run on 2026-08-12, trimmed for length: one of its 10 attached reviews, and 3 of the 8 entries in each of `tasksOffered` and `trades`. Every value below is verbatim. Note the HQ is Leander, not the Austin we searched.

```jsonc
{
  "spid": "159152389",
  "proUuid": "c8b4fab2-1c40-11ee-b010-12f6eb045c11",
  "name": "Excalibur Plumbing",
  "profileUrl": "https://www.angi.com/companylist/us/tx/leander/excalibur-plumbing-reviews-6136084.htm",
  "category": "Plumber",
  "businessType": "LEADS",

  "phone": "5122597222",
  "website": "http://www.excaliburplumbing.com",

  "street": "11880 Old 2243 W",
  "street2": null,
  "city": "Leander",
  "state": "TX",
  "zip": "78641",
  "country": "US",
  "serviceArea": "Serving Austin, TX and surrounding areas",

  "reviewCount": 2050,
  "averageRating": 4.787,
  "recommendedRate": 92,
  "ratingBreakdown": {
    "overall": 4.787, "quality": 4.831, "value": 4.586,
    "professionalism": 4.848, "punctuality": 4.879, "responsiveness": 4.862
  },
  "starCounts": { "star1": 25, "star2": 24, "star3": 69, "star4": 127, "star5": 1803 },
  "bestReviewText": "Excalibur plumbing was just perfect to work with. Professional, timely, helpful! They will be my plumber from now on.",
  "bestReviewDate": "2020-10-04T17:54:07Z",

  "isAdvertiser": true,
  "isSponsored": true,
  "isPaidPro": true,
  "isCorporateAccount": false,
  "isSuperServiceAwardWinner": false,

  "yearsInBusiness": 16,
  "inBusinessSince": 2010,
  "responseRate": 5,

  "licensed": true,
  "licenses": ["38879"],
  "bonded": false,
  "insured": false,

  "freeEstimates": true,
  "smallJobsWelcome": true,
  "emergencyServices": false,
  "warrantiesOffered": false,
  "veteranOwned": false,
  "bilingual": false,
  "offersCommercialServices": false,
  "ecoFriendly": false,
  "acceptedPayments": ["CreditCard"],

  "tasksOffered": ["Water Heater - Install or Replace", "Tankless Water Heater - Install", "Water Main - Install, Replace or Repair"],
  "trades": ["Water Heater Installation and Replacement", "Tankless Water Heater Installation", "Water Main Install, Replace or Repair"],

  "reviews": [
    {
      "reviewId": "1a48da63-4e72-4ebc-9837-e8e4f39417cf",
      "author": "Jerry B.",
      "rating": 5,
      "text": "I really needed my outside faucet replaced… He was very friendly, courteous and easy to work with.",
      "datePublished": "2024-04-04T16:55:06.201",
      "cost": 239,
      "verified": true,
      "wouldHireAgain": true,
      "categories": []
    }
  ],

  "searchCategory": "plumbing",
  "searchCity": "austin",
  "searchState": "TX",
  "sourceUrl": "https://www.angi.com/companylist/us/tx/austin/plumbing.htm",
  "scrapedAt": "2026-08-13T02:11:49.795Z"
}
```

Four fields people misread:

- `responseRate: 5` is a 0–100 percentage, not a 0–5 rating. `averageRating: 4.787` is the 0–5 one.
- `licenses` holds licence numbers exactly as Angi prints them — `"38879"` in Texas, `"TACLA00114908C"` for an HVAC licence. They are strings, not integers.
- `reviewCount: 0` and `averageRating: 0` mean Angi shows no reviews for that pro. They are not nulls and not errors.
- `tasksOffered` comes from the listing card and `trades` from the profile; they overlap but are worded differently and neither is a subset of the other.

### Input

Fields in the order they appear in the Console.

| Field | Type | Default | What it does |
|---|---|---|---|
| `mode` | select | `search` | `search` = trade + locations, crawled and paginated for you. `startUrls` = paste Angi URLs. |
| `category` | select (313) | `plumbing` | Every category Angi publishes, each with Angi's own label. `search` mode only. |
| `categoryOther` | string | — | Hand-typed slug that overrides the picker, e.g. `water-heaters`. Only needed if Angi adds a category after this Actor's last update. An unknown slug logs the closest live matches and tries anyway. |
| `locations` | stringList | `["Austin, TX"]` | `"Austin, TX"` = one city. `"TX"` or `"Texas"` = every city Angi publishes for that trade in the state. **ZIPs are not accepted** — Angi's URLs are city-based. |
| `startUrls` | requestListSources | — | Listing pages and/or `…-reviews-123456.htm` profiles. Auto-classified; duplicates fetched once. |
| `includeContactInfo` | boolean | `true` | ON = one request per contractor, adds phone, website, trades, licences, star breakdown. OFF = 12× fewer requests (23 vs 273 on 200 pros), 4× faster, no phone or website. Same price either way. |
| `includeReviews` | boolean | `false` | Attaches `reviews[]`. No extra requests — the reviews are already in the page fetched. |
| `maxReviewsPerPro` | integer | `10` | Angi's profile payload carries its 25 most recent reviews, so 25 is the ceiling: raising this to 25 gets you all of them, and anything above 25 changes nothing. |
| `onlyWithPhone` | boolean | `false` | Drops uncontactable rows before writing and before billing, and keeps crawling to fill your quota. Needs `includeContactInfo`. |
| `maxItems` | integer | `100` (prefilled `25`) | **Your hard cost ceiling.** Duplicates never count against it. `0` = no cap. |
| `proxyConfiguration` | proxy | Apify RESIDENTIAL, US | Leave it alone — see the transport ladder below. |

#### The 313 trades

The picker is generated from Angi's own geo sitemap index, so it is Angi's list, not ours — `plumbing`, `roofing`, `electrical`, `hvac`, `landscaping`, `cleaning`, `painting`, `pest-control`, `tree-service`, `handyman-service`, `garage-doors`, `water-heaters`, `drain-cleaning`, `kitchen-and-bath-remodeling`, `concrete-driveways`, `chimney-sweep`, `asbestos-removal`, `appraisals` and 295 more, each labelled the way Angi labels it — the picker shows `hvac` as *Heating and Air Conditioning Service*, `appraisals` as *Antique and Jewelry Appraisal*, `tv-antenna` as *TV Antenna Equipment Service* and `asbestos-removal` as *Asbestos Abatement*, so you never have to guess the slug.

All 313 resolved to a live Austin listing page on 2026-08-12. **16 of them returned zero pros in Austin** — `lawn-mower-repair`, `brush-chipping`, `doorbell-installation`, `dishwasher-installation`, `fence-painting`, `fire-pit-installation`, `garbage-disposal-repair`, `grill-assembly`, `install-putting-green`, `plant-watering`, `percolation-tests`, `sink-reglazing`, `sink-repair`, `smoke-detector-installers`, `trampoline-assembly`, `weed-services`. A published page with an empty list is a real state of Angi's directory, and in a big metro it usually means that trade is folded into a broader one.

### Pricing

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

| Run | Contractors | Cost |
|---|---|---|
| The prefilled first run | 25 | $0.09 |
| The 200-contractor walk in the tables above | 200 | $0.70 |
| …the same walk with `onlyWithPhone` on | 105 | $0.37 |
| Every plumber Angi lists in Austin, TX | 880 | $3.08 |
| A four-figure list | 1,000 | $3.50 |

You are charged for contractors delivered to your dataset. You are never charged for a profile that was fetched and then filtered out, never for a page that came back blocked and had to be retried, never for the Actor's proxy budget, and never twice for the same contractor — duplicates are dropped on `spid` before anything is written. Rows are charged as they are pushed, so if you hit a budget cap you get whole rows and stop, not a half-billed dataset.

`maxItems` is the cost dial: it is prefilled at 25 and `0` means no cap.

### Honest limits

- **No email addresses.** Angi publishes none, for any contractor, on any of these pages. There is no `email` field and nothing in this Actor can produce one. If you need email, `website` (32.5% deep / 70.0% on advertiser rows) is the handle to enrich from with a separate tool.
- **Phone is a paid-placement artefact, not a directory field.** 99.1% on advertisers, 0.0% on non-advertisers, 52.5% across a whole city walk. A missing phone means Angi never published one for that pro; it is not a scraping failure. If you need every row callable, set `onlyWithPhone` and pay only for the rows that are.
- **`bonded`, `veteranOwned` and `ecoFriendly` were `false` on all 350 contractors measured**, across three trades and three metros. The keys exist in Angi's data and are being read correctly — `insured` came back `true` for 5.0% from the same cards — but these three never fired in the sample. Treat them as unproven, not as a negative signal.
- **`licenses` is 26.0% on a deep run and swings hard by trade** — 50.0% Austin plumbing, 70.0% Chicago roofing, 76.0% Phoenix electrical on the first 50 of each. This is what pros filled in on Angi, not a licence registry. For an authoritative, complete licence file in one state, use `cslb-contractor-scraper` below.
- **ZIP-code search is not supported.** Angi's directory URLs are city-based, so this Actor takes cities and states and does not pretend to take ZIPs.
- **Deduplication is per run.** Within a run, `spid` guarantees no repeats. Across two runs, or across two trades that share contractors, you must dedupe on `spid` yourself.
- **US only.** Angi's `/companylist/us/…` tree is the only surface this Actor reads.
- **Text arrives as Angi stores it.** 77 of 200 `description` values contained a literal two-character `\n` sequence rather than a newline, and 222 of 1,466 review texts contained HTML entities — 129 of them the apostrophe `&#39;`. Both are passed through unaltered rather than silently rewritten.
- **City casing follows Angi** and is occasionally inconsistent (`Austin` vs `austin`). Normalise downstream if you key on it.
- **No login, no API key, no CAPTCHA-solving service.** Angi's Cloudflare wall is handled by residential IPs and backoff, not by attacking a challenge.

### How it works, and the transport ladder

Angi's category × city pages (`/companylist/us/{state}/{city}/{category}.htm`) ship their whole result set as JSON inside the Next.js RSC "Flight" stream — 10 fully structured contractors per page plus a `resultCount` and `?page=N` pagination. The contractor's own profile page adds phone, website, trades, licensing, the star distribution and the review feed. Everything is read by key path out of parsed JSON (RSC rows and JSON-LD), never by regexing the page for a number, so a figure that appears twice on a page can never land in the wrong field. State sweeps read Angi's public `sitemap/angi-geocat-{category}.xml`, which is also where the 313-entry category picker comes from.

Angi is Cloudflare-walled. Measured through Apify on 2026-08-12 against five distinct listing URLs:

| Rung | Result |
|---|---|
| No proxy at all (home broadband — *reported and then ignored; no Apify container has a residential home IP*) | 0/3 — 403 |
| Apify datacenter, rotating exit every request | 1/10 — 10% |
| Apify datacenter, a fresh session per request | 3/10 — 30% |
| **Apify RESIDENTIAL, country US, a fresh session per request** | **18/20 — 90%** |

So RESIDENTIAL + US is the default, and a blocked request is retried on a fresh residential IP after exponential backoff rather than being dropped.

Sustained load, re-measured on the platform on 2026-08-15 (run `Kcy7Ovs8Ka2mT5Wzh`): the 200-contractor run made **273 requests, 40 of them blocked and retried, and retrieved all 220 pages it needed** — 20 listing pages and 200 profiles, zero failures, 6m36s wall clock. Across the three measurement runs behind this paragraph (`vRy621xTjxcWvRWvV`, `Kcy7Ovs8Ka2mT5Wzh`, `kFDg8nMgWTqFFpzPu`, all 2026-08-15): **329 requests, 44 blocked-and-retried (13.4%), 425 contractors delivered, every run filled its quota.**

### Uniqueness

Measured on a contiguous walk — sparse offsets prove nothing — of Austin plumbing pages 1 through 20: **200 raw listing results, 200 unique `spid`, 0 duplicates (0.0%).** The 200-contractor scrape of the same range likewise produced 200/200 unique rows, and the 150-row three-city sample produced 150/150.

Deduplication happens on `spid` **before** the profile is fetched and before anything is pushed, so the dataset — and your bill — never holds the same contractor twice within a run. `spid` is Angi's stable service-provider id; the trailing number in a profile URL is not an id (it is a placeholder `1` on many unclaimed listings) and slugs go stale after a rebrand, which is why neither is used as the key.

Angi does repeat *advertisers* at the top of the ranking rather than interleaving them, which is why the duplicate rate is a clean zero here where other directories inject promoted cards mid-list.

### When a run fails

- **Zero rows throws.** A run that extracts nothing raises an error naming the category, the locations and the proxy requirement, with the HTTP call and block counts in the final log line. A green-but-empty dataset is not a state this Actor will leave you in.
- **A category with no pros in your city fails loudly** rather than returning an empty success — pick a bigger metro, or a broader trade.
- **An unrecognised category slug warns with live alternatives.** The run logs the closest matches from Angi's own published list and then tries your slug anyway.
- **`mode: "startUrls"` with no URLs throws immediately** instead of silently falling back to a search.
- **A URL that is not an Angi company-list or profile URL is logged and skipped**, not guessed at.
- **Blocks are not failures.** Each request gets up to 6 attempts on fresh residential IPs with exponential backoff; only after all 6 is a page abandoned, with a warning naming the URL.

### Who buys this

- **Field-service SaaS** (scheduling, invoicing, dispatch, CRM for trades) — the 53.0% of a city that pays Angi for leads are pros already spending money to acquire customers, and 99.1% of them publish a phone number.
- **Marketing and web agencies selling to contractors** — invert it: the non-advertiser half, plus the 67.5% of a deep run with no `website`, is a target list of pros who are not yet buying online marketing.
- **Franchise development and home-services roll-up M\&A** — filter on `yearsInBusiness`, `reviewCount` and `averageRating` to find established owner-operators; `inBusinessSince` and `licenses` qualify them.
- **Insurance, bonding, financing and materials suppliers** — `licensed`, `insured`, `bonded`, `yearsInBusiness` and trade classification in one row, by city.
- **Home-services marketplaces recruiting supply** — pull a whole state for a trade, use `serviceArea` for coverage, `starCounts` and `recommendedRate` to rank whom to approach first.
- **Competitive intelligence teams at Angi rivals** — `isAdvertiser`, `isSponsored`, `isPaidPro` and `responseRate` by metro is a map of who is buying leads and how hard.

### Sibling 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 — phone, address, lat/long, licence-verified flag, budget band, Best of Houzz badges, social links | Design-led remodel and build trades, and when you want lat/long and a budget band, which Angi does not publish |
| [thumbtack-scraper](https://apify.com/scrapersdelight/thumbtack-scraper) | Thumbtack top pros by category and US city — rating, hires, Top Pro badge, response time | Hire counts and Top Pro status. It has no phone at all, so pair it with this Actor rather than choosing between them |
| [cslb-contractor-scraper](https://apify.com/scrapersdelight/cslb-contractor-scraper) | All 243,555 licensed California contractors from the official CSLB registry — bond, workers' comp, licence status | When licensing is the point. Angi's `licenses` is self-reported and 26.0% filled; CSLB is the registry itself |
| [checkatrade-scraper](https://apify.com/scrapersdelight/checkatrade-scraper) | Verified UK tradespeople by trade and town | The UK equivalent job. Angi is US-only |

The honest one-liner: **Angi is where you go for a callable list of contractors who are actively paying for leads; CSLB is where you go when you need the licence to be true.** More at [apify.com/scrapersdelight](https://apify.com/scrapersdelight).

### FAQ

**Does this need an account or login?**
No. It reads public directory pages only, with no login, no cookies, no API key and no CAPTCHA-solving service.

**Which trades can I scrape?**
All 313 Angi publishes, from a dropdown labelled with Angi's own wording. The list was read live from Angi's geo sitemap index on 2026-08-12 and every entry resolved to a real page.

**Do I get phone numbers and email addresses?**
Phone, yes — 99.1% on advertisers, 0.0% on non-advertisers, 52.5% across a whole-city walk of 200. Email, no: Angi publishes no contractor email addresses anywhere on these pages, and this Actor does not guess them.

**Is the address the contractor's office or the city I searched?**
The contractor's own HQ. On a 200-pro Austin search only 31.0% were HQ'd in Austin; the 200 rows spanned 43 towns in all. `serviceArea` is the field that tells you where they work.

**How deep does pagination go?**
Until your `maxItems` cap or the city runs out — 10 contractors per page, and the Actor stops when it has seen the page's own `resultCount`. Austin plumbing is 880 contractors across 88 pages.

**Can I get a whole state in one run?**
Yes — put `"TX"` in `locations` and every city Angi publishes for that trade is crawled. Set `maxItems` first; Angi publishes 20,115 city pages for plumbing alone.

**Can I search by ZIP code?**
No. Angi's directory URLs are city-based, so this Actor accepts cities and states and does not pretend to accept ZIPs.

**Do I get charged for rows my filters remove?**
No. `onlyWithPhone` drops rows before they are written or billed, and duplicates are dropped on `spid` before anything is pushed. You pay $0.0035 per contractor that lands in your dataset.

**Two runs — will I get duplicates?**
Within one run, never: 200 raw results gave 200 unique `spid` on a contiguous 20-page walk. Across runs, or across two trades that share contractors, dedupe on `spid` yourself.

**Does it need a proxy?**
Yes, a residential one, and it is the default. Measured through Apify: no proxy 0/3, datacenter rotating 1/10, datacenter pinned 3/10, RESIDENTIAL US 18/20.

**Why is my run slower with contact info on?**
Because it is one request per contractor instead of one per ten. On 200 contractors, measured 2026-08-15: 273 requests and 6m36s with profiles, 23 requests and 1m40s without. The price per contractor does not change.

**Will a run ever succeed with zero rows?**
No. Zero extracted rows raises an error naming the category, the locations and the proxy requirement, with the HTTP counts attached.

**Something looks wrong — how do I debug it?**
Read the final log line: it prints contractors pushed, listing pages parsed, profiles fetched and parsed, total HTTP calls and how many were blocked-and-retried. A high blocked count with low profiles-parsed means the proxy was changed off RESIDENTIAL.

### Legal & fair use

This Actor reads public pages under `angi.com/companylist/` and Angi's own published `sitemap/` XML. It does not log in, does not solve CAPTCHAs, does not touch any path behind authentication, and collects no data that is not served to an ordinary anonymous visitor.

Rows describe businesses, but reviewer display names and review text are personal data, and contractor phone numbers may be personal data where the business is a sole trader. **You are responsible for complying with [Angi's Terms of Service](https://www.angi.com/company/terms/) and with how you use the data** — including GDPR/CCPA where they apply, and CAN-SPAM, TCPA and state do-not-call rules if you use these rows for outreach.

Angi® and Angie's List® are trademarks of their owner; this Actor is not affiliated with, endorsed by, or sponsored by Angi.

### Feedback

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

# Actor input Schema

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

SEARCH: pick a trade from the 313-entry list below plus one or more locations; Angi's category × city pages are found and paginated for you. START URLS: paste Angi listing pages and/or pro-profile pages and nothing else is needed.

## `category` (type: `string`):

The trade to search, named the way Angi names it. All 313 entries are Angi's own published categories, read live from its geo sitemap index on 2026-08-12, and each label is Angi's own wording for that service. Used only when Scrape mode is SEARCH. One trade in one big city is usually more rows than you mean to buy: Angi lists 880 plumbers in Austin, TX alone.

## `categoryOther` (type: `string`):

Escape hatch: an Angi category slug typed by hand, e.g. `water-heaters`. When it is set it OVERRIDES the picker above. You should only need it if Angi publishes a new category after this Actor's last update — all 313 in the picker resolved to live listing pages on 2026-08-12. An unrecognised slug does not fail silently: the run logs the closest published matches and then tries it anyway. Used only when Scrape mode is SEARCH.

## `locations` (type: `array`):

One entry per place. `"Austin, TX"` crawls that one city. A bare state — `"TX"` or `"Texas"` — sweeps every city Angi publishes for the chosen trade in that state, which for plumbing is a slice of the 20,115 city pages Angi publishes across 51 states; always set Max pros when you do that. Each location is crawled with the same trade, so 3 locations = 3 city crawls. ZIP codes are NOT accepted — Angi's directory URLs are city-based, and this Actor does not pretend otherwise. Note the row's address is the pro's own HQ, not the city you searched: on a 200-pro Austin search only 31.0% were headquartered in Austin, with the rest spread across 42 other HQ towns. Used only when Scrape mode is SEARCH.

## `startUrls` (type: `array`):

Paste Angi URLs directly — a first-class path that needs no trade and no location. Category listing pages (`https://www.angi.com/companylist/us/tx/austin/plumbing.htm`, paginated for you) and/or single pro profiles (`…-reviews-123456.htm`). The Actor classifies each URL itself, and the same URL pasted twice is fetched once. Used only when Scrape mode is START URLS.

## `includeContactInfo` (type: `boolean`):

ON (recommended): opens each pro's own profile to add phone, website, the full trade list, licence numbers, bonded/insured and the 1–5 star breakdown. Measured on a 200-pro contiguous walk of Austin plumbing (2026-08-12): phone 52.5% and website 32.5% overall — but 99.1% and 61.3% on the 106 advertisers, and 0.0% and 0.0% on the 94 non-advertisers. OFF: listing data only, which on the same 200 pros took 23 requests instead of 273 (12× fewer) and 1m40s instead of 6m36s (measured 2026-08-15, runs kFDg8nMgWTqFFpzPu / Kcy7Ovs8Ka2mT5Wzh) — but phone, website, trades, licences and the star breakdown are null on every row. You are billed per pro returned either way, so this saves time, not money.

## `includeReviews` (type: `boolean`):

Attaches the pro's most recent reviews (author, star rating, text, date, job cost, verified flag, would-hire-again) as a `reviews[]` array. Costs no extra requests — the reviews are already inside the profile page the Actor fetched — but rows get much larger. On the 200-pro sample 90.0% of pros carried at least one review, mean 7.54 reviews per pro at the default cap of 10. Needs the profile fetch above to be ON. Reviewer display names and review text are personal data; see the README's Legal section.

## `maxReviewsPerPro` (type: `integer`):

Caps how many reviews are attached per record when reviews are included. Angi's profile payload carries the 25 most recent reviews, so 25 is the real ceiling — the default of 10 just keeps rows small; raise it to 25 to keep every review Angi ships. Values above 25 change nothing.

## `onlyWithPhone` (type: `boolean`):

Drops any record with no phone number before it is written or billed, and keeps crawling to try to fill your Max pros quota with contactable rows. This matters more on Angi than on most directories: only the pros who pay Angi publish a phone number. On the 200-pro Austin plumbing walk this would have kept 105 rows of 200 and cut the bill from $0.70 to $0.37. Needs the profile fetch to be ON, because the listing page carries no phone at all.

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

Your hard cost ceiling: the run stops once this many pros have been delivered. You are billed $0.0035 per pro returned, so 25 = $0.09, 500 = $1.75 and 1,000 = $3.50. Duplicates are dropped before billing and never count against it. Prefilled at 25 so a first run finishes in about a minute and costs about nine cents — raise it for production. 0 = no cap (Austin plumbing alone would then run to 880 pros).

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

Leave this alone. Angi is Cloudflare-walled, and the ladder was measured through Apify on 2026-08-12: no proxy at all 0/3, Apify datacenter with a rotating exit 1/10, Apify datacenter with a fresh session per request 3/10, Apify RESIDENTIAL + country US 18/20. RESIDENTIAL + US is therefore the default and is effectively required; when a request is blocked the Actor backs off and retries on a fresh residential IP.

## Actor input object example

```json
{
  "mode": "search",
  "category": "plumbing",
  "locations": [
    "Austin, TX"
  ],
  "includeContactInfo": true,
  "includeReviews": false,
  "maxReviewsPerPro": 10,
  "onlyWithPhone": false,
  "maxItems": 25,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `records` (type: `string`):

The dataset of scraped Angi pros (one item per contractor).

# 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 = {
    "mode": "search",
    "category": "plumbing",
    "locations": [
        "Austin, TX"
    ],
    "includeContactInfo": true,
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/angi-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 = {
    "mode": "search",
    "category": "plumbing",
    "locations": ["Austin, TX"],
    "includeContactInfo": True,
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/angi-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 '{
  "mode": "search",
  "category": "plumbing",
  "locations": [
    "Austin, TX"
  ],
  "includeContactInfo": true,
  "maxItems": 25
}' |
apify call scrapersdelight/angi-scraper --silent --output-dataset

```

## MCP server setup

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