# Thumbtack Scraper - Local Service Pros Market Data (`maydit/thumbtack-local-pros-scraper`) Actor

Scrape Thumbtack service pros by category and city: ratings, review counts, hires, years in business, Top Pro status. Business-level data, no personal contact details.

- **URL**: https://apify.com/maydit/thumbtack-local-pros-scraper.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Thumbtack Scraper - Local Service Pros Market Data

Scrape **Thumbtack** local service professionals by category and city, and get back clean,
structured **business-level market data**: business name, market, star rating, review count,
lifetime hire count, years in business, employee count, average response time and Thumbtack's
**Top Pro** badge.

This is a market-research and competitive-intelligence tool. Use it to size a local service
market, benchmark your own listing against the competition, track how ratings and hire counts move
over time, or find which metros are crowded and which are thin.

**It deliberately does not collect personal contact details.** Thumbtack pros are overwhelmingly
sole proprietors, which makes their phone number, email address and street address *personal
information* under CCPA/CPRA. Other scrapers sell that data. This one does not collect it, and it
actively redacts anything phone- or email-shaped that a pro typed into a free-text field. What you
get is business-level market data you can defend using.

***

### What you get

Every row is one Thumbtack pro listing.

| Field | Type | Description |
|---|---|---|
| `businessName` | string | The business name as published on Thumbtack. Phone numbers and emails typed into the name are redacted. |
| `category` | string | The service category you asked for, as you wrote it. |
| `cityState` | string | The **market** the pro serves, e.g. `Houston, TX`. This is a metro, never a street address. |
| `rating` | number | null | Average star rating, 1-5. `null` when the pro has no reviews yet. |
| `reviewCount` | number | null | Number of reviews behind that rating. |
| `hireCount` | number | null | Lifetime hires on Thumbtack. A strong proxy for how established the business is. |
| `yearsInBusiness` | number | null | Derived from the founding year Thumbtack publishes. |
| `topPro` | boolean | `true` when Thumbtack has awarded the listing its **Top Pro** badge. |
| `servicesText` | string | null | Services listed for this pro in this category. |
| `employeeCount` | number | null | Employee count Thumbtack publishes for the business. |
| `avgResponseTimeHours` | number | null | Average hours the pro takes to respond, rounded to 0.1 h, so `0` means under about 3 minutes. `null` when Thumbtack reports exactly 0: in our checks that value appeared only on new or low-activity pros whose response time had not been measured yet, so this Actor does not pass it off as an instant response. |
| `profileUrl` | string | null | Public Thumbtack profile URL, with tracking parameters stripped. `null` in the rare case where the pro typed a phone number into their business name, because Thumbtack builds the URL slug from that name and publishing the URL would leak the number. |
| `scrapedAt` | string | ISO 8601 timestamp of collection. |

Rows are de-duplicated across the whole run: a pro who appears on several browse pages is returned
once, and you are not billed twice for them.

A `SUMMARY` record is also written to the key-value store with the row count, how many browse pages
were requested, how many listings were skipped as already-collected duplicates, which sources
returned nothing and why, and whether the run stopped on its time budget.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `categories` | string list | `["House Cleaning"]` | Thumbtack service categories, written the way Thumbtack writes them. |
| `locations` | string list | `["Houston, TX"]` | US markets in `City, ST` format. Full state names work too. |
| `maxItems` | integer | `50` | Stop after this many pros. |
| `maxRunSeconds` | integer | `240` | Wall-clock budget. On expiry the run stops cleanly and **keeps** the rows collected so far. |
| `includeNearbyAreas` | boolean | `false` | Follow Thumbtack's own "In other nearby areas" links to neighbouring browse pages. Many more pros, many more requests. |
| `requestDelayMs` | integer | `1200` | Politeness delay between requests. Requests are always serial, never parallel. |

Every category is scraped in every location, so 3 categories and 4 cities is 12 browse pages.

### Example output

```json
{
  "businessName": "Brenes Pro Cleaning Services",
  "category": "House Cleaning",
  "cityState": "Houston, TX",
  "rating": 5,
  "reviewCount": 40,
  "hireCount": 99,
  "yearsInBusiness": 1,
  "topPro": true,
  "servicesText": "House Cleaning",
  "employeeCount": 2,
  "avgResponseTimeHours": 0.1,
  "profileUrl": "https://www.thumbtack.com/tx/houston/house-cleaning/brenes-pro-cleaning-services/service/576885179750318085",
  "scrapedAt": "2026-09-24T00:46:23.880Z"
}
```

### Data source, access and limits

**Where the data comes from.** Public Thumbtack category-and-city browse pages at
`https://www.thumbtack.com/<state>/<city>/<category>` - the same pages any visitor sees, and the
same pages Thumbtack submits to search engines in its own sitemap. The listing data is read from
the structured `LocalBusiness` payload Thumbtack renders into those pages. No login, no API key and
no Thumbtack account is involved.

**robots.txt is honoured.** The Actor fetches `https://www.thumbtack.com/robots.txt` at the start of
every run, parses the `User-agent: *` group, and refuses to request any path that group disallows -
including `/profile`, `/api`, `/graphql`, `/instant-results`, `/request/` and `/bid/`. If robots.txt
cannot be fetched, a built-in snapshot of the same rules is used instead, so the guard fails closed.
Redirects are followed one hop at a time and each hop is re-checked against robots.txt, so the
Actor cannot be redirected onto a disallowed path. The browse pages this Actor reads are not
disallowed.

**It behaves politely.** Requests are serial with a delay, never parallel bursts. The User-Agent is
honest and names Mayd It LLC with a contact address; it does not impersonate a browser. HTTP 429 and
`Retry-After` are respected. There is no CAPTCHA solving, no browser-fingerprint spoofing and no
proxy rotation to evade blocks - **if Thumbtack blocks this client, the Actor reports that plainly
and stops.**

#### Known limitations - please read before you buy

- **Roughly 4-10 pros per category and city.** That is how many Thumbtack publishes on a public
  browse page. Getting more would mean using Thumbtack's internal search endpoint, which robots.txt
  disallows, so this Actor does not. To collect more pros, add more categories and cities.
- **`includeNearbyAreas` adds far less than the extra page count suggests.** Neighbouring
  suburbs mostly list the *same* pros as the metro, because pros serve the whole metro. In a
  measured run across six Los Angeles-area pages, Thumbtack listed 47 pro cards in total but only
  **13 distinct businesses** - and two of those six pages contributed no new pro at all. Expect a
  large number of requests for a modest number of extra rows. Scraping additional, genuinely
  separate metros is the better way to grow a dataset.
- **No prices and no background-check flag.** The browse pages this Actor reads do not publish
  either one: the price fields in their structured listing data were blank for every pro in every
  city and category we tested, and there is no per-pro background-check field (Thumbtack's own
  pages say every account owner must pass a background check, which is a platform rule, not a
  per-pro data point). Rather than ship columns that are always empty, this Actor leaves them out.
- **Some categories publish less detail.** On Thumbtack's Movers pages the structured listing
  data carries no founding year, employee count or service list, so `yearsInBusiness`,
  `employeeCount` and `servicesText` came back `null` for every mover we tested (Los Angeles and
  Miami). The other fields fill normally there, and the trade categories (plumbing, roofing,
  handyman, cleaning and so on) carry all three for nearly every pro.
- **No personal contact data, by design.** No phone numbers, no email addresses, no street
  addresses. If you need those, this is not the Actor for you.
- **Category names must match Thumbtack's own wording.** A category that does not exist under the
  name you gave returns HTTP 404; the run reports exactly which pair failed and keeps the rest.
- **US coverage.** The `City, ST` URL structure this Actor uses is Thumbtack's US structure.

**Billing.** This Actor is paid per result: you are charged for the rows it returns. Runs that
return no rows fail loudly with a diagnostic instead of billing you for an empty dataset.

### FAQ

**Why doesn't it return phone numbers or emails like other Thumbtack scrapers?**
Because most Thumbtack pros are sole proprietors, so their contact details are personal information
about an individual under CCPA/CPRA, not neutral business data. Collecting and reselling it creates
real obligations for whoever holds it. This Actor is built for market analysis, where you do not
need it. That is a deliberate product decision, not a missing feature.

**How many pros will I actually get?**
About 4-10 per category and city pair, because that is what Thumbtack publishes publicly. Ten
*separate* cities for one category typically lands between 40 and 100 distinct pros. Turning on
`includeNearbyAreas` sweeps the suburbs around a metro, but those pages largely repeat the same
businesses - see *Known limitations*, where the measured numbers are - so add more metros rather
than relying on nearby areas to multiply your row count.

**What happens if a category or city is wrong?**
That one pair is reported as a failed source with the reason, and the run continues with the others.
The run only fails if *every* pair failed - and then the error message names the likely cause and how
to fix it.

**What happens on a big job that cannot finish?**
The Actor watches the clock. When it reaches `maxRunSeconds` (or the platform timeout, whichever is
sooner) it stops cleanly, keeps every row already collected, and logs how many sources were left.
Raise `maxRunSeconds` or split the job across runs.

**Will this get blocked?**
It requests only pages robots.txt permits, serially, with a delay and an honest User-Agent, so it
behaves like a well-mannered crawler rather than an evasive one. If Thumbtack does block or throttle
it, the run stops and tells you - it will not try to defeat the block.

**Is the data live?**
Yes. Every run fetches the pages fresh; `scrapedAt` records when. Thumbtack's rankings and badges
shift over time, so re-running on a schedule is a reasonable way to track a market.

***

Built by **Mayd It LLC**. Questions or a field you need added: bmay@mayd-it.com

*Mayd It LLC is not affiliated with or endorsed by Thumbtack, Inc. "Thumbtack" and "Top Pro" are
trademarks of their respective owner and are used here only to describe what the data is. You are
responsible for using the output in line with applicable law and Thumbtack's terms.*

# Actor input Schema

## `categories` (type: `array`):

Thumbtack service categories to scrape, one per line, written the way Thumbtack writes them (for example "House Cleaning", "Plumbing", "Handyman", "Movers"). Names are converted to Thumbtack's URL wording automatically. If a category returns HTTP 404 it does not exist under that name - open the category page on thumbtack.com and copy the wording from its URL.

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

US markets to scrape, one per line, in "City, ST" format (for example "Houston, TX", "Los Angeles, CA"). Full state names also work. Every category is scraped in every location, so 3 categories and 4 cities means 12 browse pages.

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

Stop after this many pro records. Thumbtack's public browse page lists roughly 4-10 pros per category and city, so to collect more than that, add more categories and locations (or turn on nearby areas) rather than raising this number alone.

## `maxRunSeconds` (type: `integer`):

Wall-clock budget for the crawl. When it runs out the Actor stops cleanly and keeps every row collected so far instead of failing. Raise this for large category and location lists.

## `includeNearbyAreas` (type: `boolean`):

Follow Thumbtack's own "In other nearby areas" links to neighbouring city and neighbourhood browse pages for the same category. Nearby pages mostly list the SAME pros (in a measured Los Angeles run, 6 pages listed 47 cards but only 13 distinct businesses), so expect many more requests for a modest number of extra rows. Adding more separate metros to Locations grows a dataset faster. Leave off for a quick sample.

## `requestDelayMs` (type: `integer`):

Politeness delay between page requests. Requests are always serial, never parallel. Lower values are faster but less polite; the default is a deliberately unhurried pace.

## Actor input object example

```json
{
  "categories": [
    "House Cleaning"
  ],
  "locations": [
    "Houston, TX"
  ],
  "maxItems": 50,
  "maxRunSeconds": 240,
  "includeNearbyAreas": false,
  "requestDelayMs": 1200
}
```

# Actor output Schema

## `results` (type: `string`):

One row per Thumbtack service pro: business-level market data only (rating, reviews, hires, Top Pro), no personal contact details.

## `summary` (type: `string`):

Totals for the run, including how many records were requested versus returned and whether the run stopped early on its time budget.

# 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 = {
    "categories": [
        "House Cleaning"
    ],
    "locations": [
        "Houston, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/thumbtack-local-pros-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 = {
    "categories": ["House Cleaning"],
    "locations": ["Houston, TX"],
}

# Run the Actor and wait for it to finish
run = client.actor("maydit/thumbtack-local-pros-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 '{
  "categories": [
    "House Cleaning"
  ],
  "locations": [
    "Houston, TX"
  ]
}' |
apify call maydit/thumbtack-local-pros-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maydit/thumbtack-local-pros-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/0sJTEGa4ggVBJF0fa/builds/pRzN0Gpic2LBZCDA7/openapi.json
