Angi Scraper — Pros, Reviews & Category Coverage — $3.5/1k avatar

Angi Scraper — Pros, Reviews & Category Coverage — $3.5/1k

Under maintenance

Pricing

from $1.50 / 1,000 rows

Go to Apify Store
Angi Scraper — Pros, Reviews & Category Coverage — $3.5/1k

Angi Scraper — Pros, Reviews & Category Coverage — $3.5/1k

Under maintenance

Angi (Angie's List) scraper for home service pros: ratings, reviews, phones, licenses, plus coverage mode that counts pros per city and service to expose market gaps. Listings or coverage rows. No login or cookies. MCP-ready for AI agents. $3.5 per 1,000 records.

Pricing

from $1.50 / 1,000 rows

Rating

0.0

(0)

Developer

Fortuitous Pirate

Fortuitous Pirate

Maintained by Community

Actor stats

0

Bookmarked

40

Total users

3

Monthly active users

15 days ago

Last modified

Security

Limited permissions

Share

Angi Scraper - Contractors, Reviews & Category Coverage

Scrape Angi (formerly Angie's List) without a login, a browser or an API key. Two modes in one actor:

  • Listings: one row per home-service pro for any city and service. Rating, review count, rating breakdown, badges, years in business, response time, address, logo, photos, and the featured review. Switch on includeDetails to open each pro's profile for phone number, license numbers, bonded and insured flags, review star distribution, project price range and the latest reviews.
  • Coverage: one row per city x service that answers "does Angi have this category in this city, and how many pros does it list?" It is a market-gap tool: find the services your city has supply for that Angi barely covers, or does not cover at all.

Plain HTTP, no Playwright, so runs are fast and cheap. Runs on the Apify platform with datacenter proxy first and an automatic residential fallback when Angi's Cloudflare blocks an exit IP.

What you get

Coverage rows (one per city x service)

FieldExampleMeaning
city, state, zipSan Diego, CA, nullResolved location (ZIP codes are converted to their city)
servicehouse cleaningYour input, as typed
categoryExiststrueAngi has a category page for this service in this city
categoryStatusokok, no_pros, no_category, unknown_city or error (see below)
categoryNameHouse CleaningAngi's display name for the category
proCount961Total pros Angi lists for this city x service
resultCount10Pros returned on page 1
sponsoredOnFirstPage10How many of the first-page pros are paid placements
avgRating, ratingCount4.4, 496Average rating and rating count Angi publishes for the category page
matchedSlug, triedSlugshouse-cleaning, ["house-cleaning"]Which Angi category slug matched (up to 3 spellings are tried)
pageUrlhttps://www.angi.com/companylist/us/ca/san-diego/house-cleaning.htmThe Angi page the numbers came from
topPros["WHB Cleaning", ...]Names of the first 3 pros

Listing rows (one per pro)

GroupFields
Identityname, providerId, providerUuid, profileUrl (also url), logoUrl, photos, photosCount
Reputationrating, reviewCount, recommendedRate, ratingBreakdown (overall, quality, value, responsiveness, punctuality, professionalism where Angi shows them), featuredReview, featuredReviewDate
BadgesangiApproved, sponsored, superServiceAward, corporateAccount, unlicensedContractor, badges, awards
BusinessyearsInBusiness, inBusinessSince, introduction, services / categories, servingArea, street, city, state, postalCode
Offer flagsfreeEstimates, emergencyServices, warrantiesOffered, veteranOwned, bilingual, smallJobsWelcome, offersCommercialServices, paymentMethods, contactMethods
ResponsivenessresponseTime, responseTimeMinutes, responseRate, responseRatePercent, recentQuoteRequests (neighbors who recently requested a quote)
Search contextsearchCity, searchState, searchService, matchedSlug, searchPage, position, scrapedAt

With includeDetails (one extra profile page per pro) the row also gets:

FieldExample
phone, phoneRaw(747) 256-4174, 7472564174
licenses[{"number":"1110710","state":"CA","expires":"09/30/2027"}], plus licensed true/false
bonded, insuredfalse, false
ecoFriendlyAccreditations[]
servicesOfferedFree-text "Services we offer" from the profile
reviewDistribution{"1star":1,"2star":0,"3star":0,"4star":1,"5star":83}
priceFrom, priceRangeFrom the min and max project cost of the pro's photo albums, when present
albumsCount, highlights, categoryIds, reviewPagesProfile metadata
recentReviewsWith maxReviewsPerPro: reviewer, rating, date, text, job, cost, verified, proResponse

What Angi does not publish

Said plainly so you do not buy something you will not get:

  • Website URLs are not on Angi's public pages, so there is no website field.
  • Business hours are empty in the data Angi serves for every pro we sampled, so there are no hours.
  • Letter grades (A-F) are not in the public pages. You get the numeric rating breakdown and the star distribution instead.
  • Reviews stop at 25 per pro. maxReviewsPerPro is capped at 25, the latest page on the profile.
  • Angi has no ZIP-level pages. A ZIP code is resolved to its city and the city page is scraped.
  • Angi only has home-service categories (314 of them). It has no cake, dog grooming or other non-home categories, which is exactly what coverage mode will tell you.

Coverage mode: find the gaps

Coverage mode costs one page request per city x service and returns a single row per pair, even when nothing matches, so a gap is always visible in the output.

categoryStatuscategoryExistsMeaning
oktrueCategory page exists and lists pros. proCount is Angi's number.
no_prostrueThe category exists in that city but lists 0 pros (example: pool and spa service in Nome, AK).
no_categoryfalseNone of the tried slugs exist on Angi, for example a service Angi does not cover at all.
unknown_cityfalseAngi has no page for that city under the spelling given. Check the spelling or try a nearby city.
errornullThe request failed (blocked, timeout). The row is kept so the grid stays complete, and it is not charged.

A typical workflow: take the services your local directory, Yelp export or Google Maps scrape says exist in your city, run them through coverage mode for the same city, and sort by proCount. Services with no_category, no_pros or a proCount in single digits are places where a marketplace offers customers little, and where a local provider, lead-gen site or agency has room.

Real coverage rows, from the default {} run (San Diego x cake, dog grooming, house cleaning):

{
"recordType": "coverage",
"marketplace": "angi",
"city": "San Diego",
"state": "CA",
"zip": null,
"service": "house cleaning",
"categoryExists": true,
"categoryStatus": "ok",
"categoryName": "House Cleaning",
"proCount": 961,
"resultCount": 10,
"sponsoredOnFirstPage": 10,
"avgRating": 4.4,
"ratingCount": 496,
"pageUrl": "https://www.angi.com/companylist/us/ca/san-diego/house-cleaning.htm",
"matchedSlug": "house-cleaning",
"triedSlugs": ["house-cleaning"],
"topPros": [
"At Your Service Construction Clean and Home Detail - Unlicensed Contractor",
"WHB Cleaning",
"Del Sol Cleaning Services"
],
"scrapedAt": "2026-10-22T02:09:54.241Z"
}
{
"recordType": "coverage",
"marketplace": "angi",
"city": "San Diego",
"state": "CA",
"zip": null,
"service": "cake",
"categoryExists": false,
"categoryStatus": "no_category",
"categoryName": null,
"proCount": null,
"resultCount": 0,
"sponsoredOnFirstPage": 0,
"avgRating": null,
"ratingCount": null,
"pageUrl": null,
"matchedSlug": null,
"triedSlugs": ["cake"],
"topPros": [],
"scrapedAt": "2026-10-22T02:09:54.241Z"
}

Input

InputTypeDefaultMeaning
modelistings / coveragesee belowRow per pro, or row per city x service
citiesstring[]-"San Diego, CA", "Austin, Texas", "Austin TX" or a ZIP code
servicesstring[]-Plain words ("plumbers", "pool cleaning", "junk removal") or an Angi slug ("pest-control"). Fuzzy-matched over 314 Angi category slugs; up to 3 slugs are tried
zipCodesstring[]-ZIP codes, converted to their city
categorystring-One Angi slug; same as adding it to services
startUrlsURL list-Angi category pages or pro profile pages
listUrlstring-Public CSV or Google Sheet; each row a city, a city plus service, or an Angi URL
maxItemsinteger10Max pros per city x service (Angi shows 10 per page)
includeDetailsbooleanfalseOpen each pro's profile for phone, licenses and more
maxReviewsPerProinteger0Latest reviews per pro (0-25), used with includeDetails or profile URLs
concurrencyinteger3Parallel searches
residentialFallbackbooleantrueRetry blocked requests through Apify RESIDENTIAL (US)
proxyConfigurationproxyApify datacenterFirst proxy tier tried
state, city, servicestring-Legacy single search (state code, city slug, service slug). Still works
maxResultsinteger-Legacy alias of maxItems

Which mode runs? If you set mode, that one. With no input at all the actor runs a coverage sample (San Diego x cake, dog grooming, house cleaning) so the first test run returns data in seconds. With cities/services, startUrls or the legacy fields and no mode, it runs listings.

Example 1: coverage grid for a city

{
"mode": "coverage",
"cities": ["San Diego, CA"],
"services": ["house cleaning", "dog grooming", "roofers", "pool cleaning", "wedding cake"]
}

Five rows, one per service.

Example 2: coverage across many cities from a sheet

{
"mode": "coverage",
"listUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/export?format=csv"
}

Each sheet row is "city, state, service" (for example Nome,AK,pool cleaning) or just a city to be combined with the services input.

Example 3: roofers with phone numbers and reviews

{
"mode": "listings",
"cities": ["Austin, TX", "92109"],
"services": ["roofers"],
"maxItems": 5,
"includeDetails": true,
"maxReviewsPerPro": 2
}

Up to 5 pros per city, each with phone, licenses, review distribution and the 2 latest reviews. The ZIP 92109 resolves to San Diego.

Example 4: specific pages

{
"startUrls": [
{ "url": "https://www.angi.com/companylist/us/ca/julian/chimney-sweep.htm" },
{ "url": "https://www.angi.com/companylist/us/ca/chula-vista/primo-plumbing-reviews-1.htm" }
],
"maxReviewsPerPro": 5
}

A category page returns its listings; a profile URL returns that pro's profile row with details and reviews.

{ "state": "ca", "city": "san-francisco", "service": "plumbing", "maxResults": 12 }

Existing saved inputs keep working and return listing rows with the same field names as before, plus the new ones.

Real listing row

From a cloud run with includeDetails (long arrays shortened here for readability only):

{
"recordType": "listing",
"marketplace": "angi",
"name": "Clear View Roofing & Construction",
"rating": 4.89,
"reviewCount": 115,
"recommendedRate": 50,
"ratingBreakdown": { "overall": 4.94, "responsiveness": 5, "quality": 5, "value": 5 },
"services": ["Asphalt Shingle Roofing - Install or Replace", "Asphalt Shingle Roofing - Repair", "... (27 total)"],
"servingArea": "Serving San Diego, CA and surrounding areas",
"angiApproved": true,
"sponsored": true,
"superServiceAward": false,
"unlicensedContractor": false,
"badges": ["Angi Approved", "Sponsored"],
"yearsInBusiness": 36,
"inBusinessSince": 1990,
"freeEstimates": true,
"emergencyServices": true,
"warrantiesOffered": true,
"bilingual": true,
"paymentMethods": ["CREDIT_CARD"],
"responseTime": "7 hrs",
"responseTimeMinutes": 385,
"responseRate": "100%",
"recentQuoteRequests": 564,
"contactMethods": ["CALL_DIRECT", "CALL_REQUEST", "MESSAGE", "CONTACT_NOW"],
"street": "12520 Kirkham Ct Suite 2",
"city": "Poway",
"state": "CA",
"postalCode": "92064",
"logoUrl": "https://cdn.homeadvisor.com/files/eid/141780000/141784072/20617544_logo.png",
"photosCount": 2,
"phone": "(747) 256-4174",
"providerId": "141784072",
"profileUrl": "https://www.angi.com/companylist/us/ca/san-diego/clear-view-roofing-construction-reviews-1.htm",
"searchCity": "San Diego",
"searchState": "CA",
"searchService": "roofers",
"matchedSlug": "roofing",
"position": 5,
"licenses": [{ "number": "1110710", "state": "CA", "authority": null, "status": null, "expires": "09/30/2027" }],
"licensed": true,
"bonded": false,
"insured": false,
"reviewDistribution": { "1star": 1, "2star": 0, "3star": 0, "4star": 1, "5star": 83 },
"albumsCount": 2,
"recentReviews": [
{
"reviewer": "anna S.",
"rating": 5,
"date": "October 2026",
"text": "I couldn't be happier with how my tile roof turned out! ...",
"job": "Traditional Tile Roofing - Install or Replace",
"cost": "$18,000",
"verified": true,
"proResponse": null
}
],
"scrapedAt": "2026-10-22T02:10:20.101Z"
}

Note that licensed: true means a license number is on the profile. insured and bonded reflect what the pro has published on Angi, so false means "not shown", not "proven uninsured".

Use cases

  1. Local market gap analysis. Compare your city's real service supply (Yelp, Google Maps) against what Angi covers. Missing or single-digit categories are openings for a local directory, a lead-gen site or a new service line.
  2. Contractor lead lists. Pull pros by trade and city with phone, license number, years in business and rating, filter by rating and review count, and load the result into a CRM.
  3. Competitor monitoring for a service business. Track how many pros, how many sponsored placements and what average rating Angi shows for your trade in your city, week over week, with a schedule.
  4. Reputation benchmarking. Compare your rating, review count, response time and response rate against the pros that rank above you in your city.
  5. Sales targeting for software and insurance vendors. Find unlicensed, uninsured or unbonded contractors, or high-volume pros that get hundreds of quote requests.
  6. Market sizing for franchises and roll-ups. proCount per city x service gives a quick count of how many pros compete in a category, across a whole list of cities.
  7. Review research. Latest reviews with job type, cost and verified flag, for sentiment or price analysis.

Pricing

Pay per event. You pay the start fee plus the rows that are saved to the dataset. Both modes bill the same per-row price, and coverage rows are rows.

Until 5 Nov 2026From 6 Nov 2026
Start fee (per run, 512 MB)$0.005$0.001
Per row (listing or coverage)$0.0035$0.0015
Cost of 1,000 rows$3.50$1.50
profile-details add-on, per pro whose profile is opened with includeDetails-$0.002
Cost of 1,000 pros with details (phone, licenses, reviews)-$3.50

Coverage rows that fail (categoryStatus: "error") are saved so your grid stays complete, but they are not charged. Use maxItems and the Apify "max cost per run" setting to cap spend.

At the 6 Nov 2026 prices: a coverage grid of 50 cities x 20 services is 1,000 rows, $1.50 plus the start fee. 100 roofers with phone numbers is 100 rows plus 100 detail events, $0.35.

How it works and limits

  • Angi serves its category and profile pages through server-rendered Next.js data. The actor reads that embedded data directly, so there is no browser and no page rendering, and a run uses about 0.8 MB per search page and 0.4 MB per profile.
  • Angi sits behind Cloudflare. The actor sends requests with a browser-like TLS fingerprint, tries the proxy you configure (Apify datacenter by default), and if a request is blocked, retries without proxy and then through Apify RESIDENTIAL (US). Which exit IPs get through varies, so residential fallback is on by default. Switch it off with residentialFallback: false if you want to avoid residential traffic costs on your Apify bill.
  • Requests are throttled so one exit IP is not rate-limited. A very large grid is better split across several runs or run with concurrency at the default.
  • Partial results are kept. If part of a run is blocked, finished rows stay in the dataset and the OUTPUT record in the key-value store holds a per-search summary (status, rows, failures, proxy tier used).
  • City spelling matters. Angi uses st-louis and manhattan, not "Saint Louis" or "New York" in some places. If you get unknown_city, try a nearby spelling or a neighboring city.
  • Angi shows 10 pros per page; maxItems above 10 pages through results and stops at the last page.

FAQ

Is this the official Angi API? No. It reads Angi's public web pages. No login, cookies or account are used or needed.

Why is website missing? Angi's public profile pages do not show a business website. The phone number is what Angi displays, and it can differ from a number written in the pro's own description.

Why does a service return no_category? Angi has 314 home-service categories. A service outside them (cake, dog grooming) has no page. That result is the answer coverage mode exists to give.

Does proCount equal the number of pros I can scrape? proCount is the total Angi reports for the city x category. Listings mode returns up to maxItems of them, 10 per page.

Can I get all of a pro's reviews? Only the latest 25 per pro.

Can I search by ZIP? Yes, as a city: the ZIP is converted to its city and that city's page is scraped.

Will my old saved input still work? Yes. state, city, service and maxResults are kept, and the original listing field names (name, rating, reviewCount, services, servingArea, angiApproved, inBusinessSince, freeEstimates, emergencyServices, responseTime, responseRate, featuredReview, providerId, url) are unchanged. responseTime is now formatted from minutes (for example "3 hrs", "1 day").

Integrations

Run it from the Apify Console, on a schedule, or through the API.

API (curl)

curl -s -X POST "https://api.apify.com/v2/acts/fortuitous_pirate~angi-home-services-contractors-reviews/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"coverage","cities":["San Diego, CA"],"services":["plumbing","dog grooming"]}'

Python

from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("fortuitous_pirate/angi-home-services-contractors-reviews").call(run_input={
"mode": "listings",
"cities": ["Austin, TX"],
"services": ["plumbers"],
"maxItems": 20,
"includeDetails": True,
})
for pro in client.dataset(run["defaultDatasetId"]).iterate_items():
print(pro["name"], pro["rating"], pro.get("phone"))

JavaScript

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('fortuitous_pirate/angi-home-services-contractors-reviews').call({
mode: 'coverage',
cities: ['San Diego, CA', 'Austin, TX'],
services: ['house cleaning', 'roofers'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map(r => [r.city, r.service, r.categoryStatus, r.proCount]));
  • Schedules: run a coverage grid weekly and compare proCount and sponsoredOnFirstPage over time.
  • Webhooks: call your endpoint when a run succeeds.
  • Make, Zapier, n8n: use the Apify app or HTTP module with the run-sync call above, then send rows to Google Sheets, Airtable or a CRM.
  • Exports: JSON, CSV, Excel, XML or HTML from the dataset.
  • MCP / AI agents: the actor is available through the Apify MCP server, so an AI agent can call it with the same input as above.

Support

Open an issue on the actor's Issues tab with the input you used and the run link.