# Checkatrade Scraper — UK Tradespeople, Ratings & Reviews (`memo23/checkatrade-scraper`) Actor

Checkatrade scraper — search any trade + town, or paste profile/search URLs. One flat row per tradesperson: phone, locality, score out of 10, review counts, skills, badges, vetting and member-since. Opt-in full review history from Checkatrade's public API. Pure HTTP, no browser.

- **URL**: https://apify.com/memo23/checkatrade-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Lead generation, AI, Agents
- **Stats:** 25 total users, 2 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 tradespeople

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

## Checkatrade Scraper — UK Tradespeople, Ratings & Reviews

Turn any [Checkatrade](https://www.checkatrade.com) search into structured data. Search by **trade + location** (*Plumber in London*), or paste **search-listing** and **trade-profile** URLs — and get one flat row per tradesperson: name, phone, locality, score out of 10, review counts, skills and sub-skills, badges, vetting flags, membership and profile URL. Turn on deep reviews and each row also carries that trade's **full customer review history**, straight from Checkatrade's own public reviews API.

#### How it works

![How the Checkatrade Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-checkatrade.png)

***

### Why use this scraper?

- **Rich rows with nothing switched on.** ~36 fields per tradesperson straight from the search listing: company name, phone (formatted + E.164), locality/region, recent score out of 10, recent + all-time review counts, every skill group with its sub-skills, quality and vetting badges, sponsorship flag, logo and gallery images, profile URL.
- **The full review history, not the six on the page.** A profile page embeds at most 6 recent reviews. Set `maxReviewsPerTrade` above 0 and the actor pulls the complete history from Checkatrade's public reviews API — with per-dimension scores (workmanship, reliability, communication), reviewer name, work category, price band, the trade's reply and review photos.
- **Gets past the Cloudflare wall.** Checkatrade fronts the site with a Cloudflare managed challenge. Every page request goes out with a real browser TLS/HTTP-2 fingerprint over GB residential IPs, rotating the session on each retry, so you get the real page instead of "Just a moment…".
- **Two ways in, mixed freely.** Build searches from a trade + a list of towns, paste search-listing URLs, paste profile URLs — or all three in one run. Results are deduplicated by company across every source.
- **Not brittle DOM scraping.** Rows are parsed out of Checkatrade's embedded Next.js data stream, so a layout change doesn't silently empty your dataset.
- **Costs are capped before the first request.** The run trims itself to what your maximum-cost setting can actually pay for, so you never burn proxy bandwidth producing rows the charge limit will not bill.

***

### Overview

One dataset row = **one tradesperson (company)**, never one review and never one page. Reviews, skills, services and images are nested arrays inside that row, so a run of 500 tradespeople gives you exactly 500 rows however deep you scrape.

Scores are Checkatrade's own scale — **out of 10**, not out of 5. `rating` is the *recent* mean score; `reviewMetrics.totalScore` is the all-time one.

***

### Supported inputs

| Input | Example | What it does |
|---|---|---|
| Trade + location | `trade: "Plumber"`, `locations: ["London", "Leeds"]` | Builds `/Search/Plumber/in/London` for each town and paginates it |
| Trade with no location | `trade: "Electrician"`, `locations: []` | Falls back to a UK-wide search |
| Search-listing URL | `https://www.checkatrade.com/Search/Builder/in/London` | Paginates that trade/location's results |
| Profile URL | `https://www.checkatrade.com/trades/gem247` | Scrapes that single tradesperson, always in full detail |

Search and profile URLs are auto-classified — you do not have to sort them. Anything that is not a `checkatrade.com` `/Search/…` or `/trades/{slug}` URL is reported in the log and skipped.

**Not supported:** anything behind a Checkatrade login, buyer messages, quote and booking flows, and account-only data. The actor reads public search-listing pages, public profile pages and the public reviews API.

***

### Use cases

| You are a… | Use it to… |
|---|---|
| Market researcher | Measure score and review-volume distribution, and trade density, across UK towns |
| Trades business owner | Benchmark competitors in your patch — scores, review counts, skills, badges, membership age |
| Reputation / review analyst | Pull a trade's full review history with per-dimension scores, categories and replies |
| Directory or comparison site | Seed a UK tradesperson dataset with structured company, rating and skill rows |
| Sales / lead-gen team | Build a call list by trade + town — phone numbers are public on most listings (see the honest note on emails below) |
| Recruiter | Find verified tradespeople by skill, sub-skill and location |

***

### How it works

1. **Classify.** A trade + each location becomes a `/Search/{Trade}/in/{Location}` URL; pasted URLs are sorted into search vs profile.
2. **Fetch.** Each page is requested with a real browser TLS fingerprint over a GB residential IP. A Cloudflare challenge or a 403/429/5xx rotates to a fresh session and retries (15 attempts by default).
3. **Parse.** The page's embedded Next.js RSC data stream is reassembled and every company object extracted — a search page carries ~11–12 tradespeople and reports its own `totalResults`/`totalPages`, which drives `?page=N` pagination up to your cap.
4. **Detail (optional).** With `scrapeDetails` on, each tradesperson's profile page is fetched for the full description, the complete skill list, services offered, review metrics, up to 6 embedded reviews and — where the trade published one — their own website.
5. **Deep reviews (optional).** With `maxReviewsPerTrade` above 0, the full review history is pulled from `api.checkatrade.com/v1/consumer-public/reviews` — 25 per request, no auth, no Cloudflare, so it is fetched directly without spending residential bandwidth. It needs only the company ID that listing rows already carry, so it works with `scrapeDetails` off and adds zero profile fetches.
6. **Emit.** One flat, deduplicated row per tradesperson, charged once.

***

### Input configuration

| Field | Type | Default | Notes |
|---|---|---|---|
| `trade` | string | — | Trade/category to search, e.g. `Plumber`, `Electrician`, `Gas Boiler Servicing Repair`. Combined with every location. |
| `locations` | array | — | Towns/cities to search, e.g. `["London", "Manchester"]`. Empty + a trade set = UK-wide. |
| `startUrls` | array | `[]` | Search-listing and/or profile URLs. Auto-classified, mixable with `trade`. |
| `scrapeDetails` | boolean | `false` | Fetch each profile page for the full description, all skills, services, review metrics, up to 6 embedded reviews and the trade's own website. ~2× the requests. Pasted profile URLs are always detailed. |
| `maxReviewsPerTrade` | integer | `0` | `0` = only the up-to-6 reviews embedded in the profile page (needs `scrapeDetails`). Above `0` = that many from the public reviews API, 25 per request, works with `scrapeDetails` off. |
| `enrichEmails` | boolean | `false` | Opt-in, best-effort contact-email discovery. Read the limitations section before switching it on — the realistic hit rate is low. |
| `maxItems` | integer | `1000` | Hard cap on rows for the whole run. Free-tier accounts are additionally capped at 100. |
| `maxItemsPerSearch` | integer | `1000` | Cap per trade+location search. Checkatrade paginates ~12 per page, 60 pages max per search. |
| `maxConcurrency` | integer | `6` | Parallel requests. Keep moderate (4–8) — the site is Cloudflare-protected. |
| `maxRequestRetries` | integer | `15` | Retry budget per request. Each retry takes a fresh GB residential IP. |
| `proxy` | object | built-in GB residential | Leave empty. The actor ships its own GB residential routing at no extra cost; set this only to force your own proxies. |

#### Example input

```json
{
  "trade": "Plumber",
  "locations": ["London"],
  "scrapeDetails": true,
  "maxReviewsPerTrade": 5,
  "maxItems": 6
}
```

That exact input is the actor's smoke test: 6 tradespeople in 20 seconds, 10 page requests plus 6 review-API requests, nothing blocked.

***

### Output overview

Every row is `rowType: "tradesperson"`. Listing fields are always present. Detail fields (`fullDescription`, `allSkillLabels`, `servicesOffered`, `reviewMetrics`, `website`, embedded `reviews`) appear when `scrapeDetails` is on or when you pasted a profile URL. Deep `reviews` from the API appear whenever `maxReviewsPerTrade` is above 0, with or without profile scraping.

Each review carries a `source` field: `"profile"` (the free embedded copy, max 6) or `"api"` (the deep history — strictly richer, with per-dimension scores, reviewer name, reply date and photos).

***

### Output samples

#### Tradesperson row (search + `scrapeDetails` + deep reviews)

```jsonc
{
  "rowType": "tradesperson",
  "companyId": "9730955",
  "name": "GEM 24/7",
  "slug": "gem247",
  "profileUrl": "https://www.checkatrade.com/trades/gem247",
  "summary": "GEM Environmental Building Services is a plumbing, heating and engineering company with over 30 years of experience…",
  "logoUrl": "https://lh3.googleusercontent.com/Ah-u3o-y6CP%E2%80%A6",
  "heroBannerUrl": "https://lh3.googleusercontent.com/DdHO6rK41Qs%E2%80%A6",
  "locality": "London",
  "region": null,
  "operatingLocation": "Operates in your area",
  "rating": 9.59,                       // recent mean score, out of 10
  "recentReviewsCount": 9,
  "totalReviews": 9,
  "phone": "07577 412278",
  "phoneRaw": "+447577412278",          // E.164; null on some listings
  "skills": [
    { "id": 20, "label": "Plumber", "subSkills": ["Blocked Toilets", "Emergency Plumber Service", "Power Flushing"] },
    { "id": 10, "label": "Central Heating", "subSkills": ["Gas Boiler Installation", "Gas Safety Checks - CP12"] }
  ],
  "skillLabels": ["Central Heating", "Electrician", "Plumber"],
  "badges": [],                         // e.g. ["Sponsored"] on paid placements
  "qualityBadges": [],                  // e.g. ["GREAT_COMMUNICATOR"]
  "trackingBadges": ["new_member", "guaranteed"],
  "isSponsored": false,
  "memberSince": null,                  // elsewhere: "Approved member since February 2013" — a sentence, not a date
  "acquiredBadge": "None",              // e.g. "fiveYear" / "tenYear"
  "images": ["https://lh3.googleusercontent.com/EVd21ZqUkDe%E2%80%A6"],
  "searchTrade": "Plumber",
  "searchLocation": "London",
  "sourceUrl": "https://www.checkatrade.com/Search/Plumber/in/London",
  "sourceMode": "search",
  "scrapedAt": "2026-08-12T20:15:47.674Z",

  // ---- detail fields (scrapeDetails / profile URLs) ----
  "detailScraped": true,
  "fullDescription": "GEM Environmental Building Services is a plumbing, heating and engineering company with over 30 years…",
  "allSkillLabels": ["Central Heating", "Emergency Central Heating Repair", "Gas Boiler Installation"],
  "servicesOffered": [],                // e.g. ["Domestic Work", "Free Estimates", "24 Hour Call-out"]
  "reviewMetrics": { "totalReviewCount": 9, "totalScore": 9.59, "recentReviewCount": 9, "workmanship": 9.44, "reliability": 9.78, "courtesy": 0, "communication": 9.56 },
  "reviewsTotal": 9,
  "website": "https://gemenvironmentalbuildingservices.co.uk/",  // present on roughly 1 profile in 3
  "websiteDomain": "gemenvironmentalbuildingservices.co.uk",
  "hasWebsite": true,
  "websiteIsBuilderOrSocial": false,
  "reviews": [ /* see below */ ]
}
```

#### Review from the public API (`maxReviewsPerTrade > 0`)

```jsonc
{
  "id": "019fc885-7dcf-7fbf-a310-1afd7c05c13d",
  "title": "Kitchen tap replacement",
  "body": "Excellent communication, excellent service and plumbing work was completed on time.",
  "createdAt": "2026-08-03T16:47:00.439Z",
  "publishedAt": "2026-08-04T17:00:09.033Z",
  "rating": 10,                       // out of 10
  "ratingWorkmanship": 10,
  "ratingReliability": 10,
  "ratingCommunication": 10,
  "verified": true,
  "reviewerName": "Shiv M",           // null when the reviewer is anonymous
  "reviewerProfileUrl": "/profile/019c9022-bc39-7213-9e89-c4a7a394db1c",
  "location": "E14",                  // outward postcode
  "category": "Tap Repair",
  "categoryId": 1591,
  "tradeReply": "Hi Shiv, thank you for your 10/10 review…",
  "tradeReplyDate": "2026-08-05T09:56:45.299Z",
  "priceBand": null,
  "hiredViaCheckatrade": true,
  "images": ["https://api.checkatrade.com/v1/media-service/media/019fc885-7d5d-797f-92ff-105cee54d4c8/content"],
  "source": "api"
}
```

***

### Key output fields

| Field | Type | Notes |
|---|---|---|
| `companyId` / `slug` / `profileUrl` | string | Checkatrade's numeric company ID, URL slug and canonical profile URL. `companyId` is the dedup key. |
| `name` / `summary` / `fullDescription` | string | Company name, short listing blurb, full profile description (detail only — `summary` is the reliable one, see limitations) |
| `phone` / `phoneRaw` | string | Public number as displayed + E.164 form. `phoneRaw` is null on some listings. |
| `locality` / `region` / `operatingLocation` | string | Town, county, and Checkatrade's coverage line |
| `rating` | number | Recent mean score, **out of 10** |
| `recentReviewsCount` / `totalReviews` | number | Recent window and all-time review counts |
| `reviewMetrics` | object | Per-dimension all-time scores: workmanship, reliability, courtesy, communication (detail only) |
| `reviews` | array | Up to 6 embedded (`source: "profile"`) or the full history (`source: "api"`) |
| `skills` / `skillLabels` / `allSkillLabels` | array | Skill groups with sub-skills, flattened group labels, and the complete label list from the profile |
| `servicesOffered` | string\[] | e.g. Domestic Work, Free Estimates, 24 Hour Call-out (detail only) |
| `qualityBadges` / `trackingBadges` / `badges` | string\[] | Quality awards, vetting/insurance flags, listing badges |
| `isSponsored` / `memberSince` / `acquiredBadge` | mixed | Paid-placement flag, membership sentence, longevity badge |
| `website` / `websiteDomain` / `hasWebsite` / `websiteIsBuilderOrSocial` | mixed | The trade's own site when their profile exposes one — see limitations |
| `logoUrl` / `heroBannerUrl` / `images` | string / string\[] | Logo, banner and gallery photos |
| `searchTrade` / `searchLocation` / `sourceUrl` / `sourceMode` | string | Which search produced the row, and whether it came from a listing or a profile |
| `contactEmail` / `contactWebsite` / `emailEnrichment` | mixed | Only with `enrichEmails` on, and only when something is actually found |

***

### Pricing

Pay-per-event. You pay for rows, not for time.

| Event | When it fires | Price |
|---|---|---|
| Actor start | Once per run | $0.005 |
| Tradesperson | Each row pushed to the dataset | $0.004 |
| Contact email found | Only when `enrichEmails` actually returns an email | $0.05 |

1,000 tradespeople ≈ **$4.01** all-in. Pagination, retries, proxy bandwidth, profile fetches and deep review-API calls are included — they are never billed separately, so a row with 400 reviews attached costs the same as a bare listing row.

The email event is priced at exactly $0.05 on purpose. The shared enrichment module bills a found email as `round($0.05 ÷ unit price)` units of `additional-data`; setting the unit price to $0.05 makes that exactly **one** unit, so a found email is one $0.05 charge and never a multiple.

Nothing is charged for a tradesperson the run does not emit. If your maximum-cost-per-run setting cannot pay for a single row, the run stops immediately and tells you, instead of scraping and discarding.

***

### Notes & limitations

Read this before you build a workflow on it.

- **This is a reviews and public-listing scraper, not a finished lead list.** Phones are public and present on most listings; email addresses mostly are not (below).
- **Website coverage is partial by design.** Checkatrade only renders a trade's own website when that profile has the flag set — measured live, **25–35% of profiles** expose one, and roughly **30% of those domains are dead or stale**. That leaves an effective, usable website rate of about **17–18%**. `website` is `null` rather than guessed when there is none.
- **`enrichEmails` is experimental and often returns nothing.** It seeds from the trade's own website when one exists, otherwise it tries to discover a domain from the business name. In a 7-row test it found **0 emails**. It is off by default, it only bills when it actually finds an address, and it should be treated as an occasional bonus — not as the reason to run this actor.
- **Reviews out of 10, not 5.** Every score — row-level and per-review — is on Checkatrade's 10-point scale.
- **Deep reviews cost requests, not extra charges.** `maxReviewsPerTrade: 400` is 16 API calls for that row. Runtime grows; the per-row price does not.
- **Pagination has a ceiling.** Each search follows up to 60 pages (~700 tradespeople). Split a big trade across towns to go wider.
- **Cloudflare is flaky, not impossible.** A blocked request rotates IP and retries. If Checkatrade blocks *every* request, the run fails loudly with the block count rather than silently reporting zero rows — re-running usually gets a session that works.
- **`memberSince` is a sentence, not a date.** Checkatrade publishes it as "Approved member since February 2013"; some profiles have none.
- **`fullDescription` is not reliable on every profile.** On some profiles Checkatrade's data stream stores the description as an internal reference (the field then reads `"$38"` rather than prose — it happened on 3 of 6 profiles in the last test run). `summary` always carries readable text, so use it as the fallback.
- **`courtesy` in `reviewMetrics` is frequently 0** — that is Checkatrade's own data, not a parsing miss.
- **Free-tier accounts are capped at 100 rows per run.**

***

### FAQ

**Do I get the tradesperson's phone number?** Yes — the public number is on most listings, both as displayed (`07577 412278`) and E.164 (`+447577412278`). A minority of listings have no E.164 form; `phoneRaw` is then null.

**Do I get their email address?** Usually not. Checkatrade does not publish emails, and the optional enrichment can only work from a website the trade published — which is about a third of profiles, of which many domains are dead. Plan for phone-first outreach.

**How many tradespeople are behind one search?** Checkatrade returns ~12 per page and reports its own total: *Plumber in London* was 133 tradespeople across 12 pages when this README was written. Use `maxItemsPerSearch` to cap it.

**Do I need `scrapeDetails` to get reviews?** No. `maxReviewsPerTrade` uses the public reviews API keyed on the company ID that listing rows already carry, so deep reviews work with profile scraping off. `scrapeDetails` is what adds the description, full skill list, services, review metrics and the trade's website.

**What is the difference between the two review sources?** `source: "profile"` is the free copy embedded in the profile page — max 6, no per-dimension scores. `source: "api"` is the full history with workmanship/reliability/communication scores, reviewer name, category, trade reply and photos.

**Do I need to configure proxies?** No. GB residential routing is built in at no extra cost. The `proxy` input exists only if you insist on your own pool.

**Which locations work?** Any town, city or area Checkatrade supports — `London`, `Manchester`, `Birmingham`, `Leeds`, `Milton Keynes`. Multi-word places are Title-Cased and dash-joined automatically.

**Why did my run return 0 rows?** If Cloudflare blocked everything, the run fails with the block count in the message — re-run it. If the input was a trade/town combination Checkatrade has no results for, the log says so per search.

**Can I re-run this on a schedule?** Yes. Rows are deduplicated within a run by company ID; across runs, dedupe on `companyId` on your side.

***

### 💬 Support

Found a bug or need an extra field? Open an issue on the [actor's Issues tab](https://apify.com/memo23/checkatrade-scraper/issues) — issues are read and answered.

### 🛠 Additional services

Need a custom field, another UK trades directory (Rated People, MyBuilder, TrustATrader), or a scheduled tradesperson feed into your CRM or warehouse? Custom scraping and pipeline work is available on request.

### 🔎 Explore more scrapers

More directory, local-business and review scrapers — Capterra, G2, Trustpilot-style review sources, job boards and lead directories — are published under the same account on the Apify Store.

***

### 🤖 For AI Agents & LLM Apps

**Purpose:** extract UK tradespeople from Checkatrade — company profile, contact phone, ratings and full customer review history.

**Minimal tested input:**

```json
{ "trade": "Plumber", "locations": ["London"], "maxItems": 6, "scrapeDetails": true, "maxReviewsPerTrade": 5 }
```

**Also accepts:** `startUrls` with `https://www.checkatrade.com/Search/{Trade}/in/{Location}` and/or `https://www.checkatrade.com/trades/{slug}` (auto-classified, mixable with `trade`).

**Output:** one flat JSON row per tradesperson, `rowType: "tradesperson"`. Fields: `companyId, name, slug, profileUrl, summary, logoUrl, heroBannerUrl, locality, region, operatingLocation, rating, recentReviewsCount, totalReviews, phone, phoneRaw, skills[], skillLabels[], badges[], qualityBadges[], trackingBadges[], isSponsored, memberSince, acquiredBadge, images[], searchTrade, searchLocation, sourceUrl, sourceMode, scrapedAt`. With `scrapeDetails`: `detailScraped, fullDescription, allSkillLabels[], servicesOffered[], reviewMetrics{}, reviewsTotal, website, websiteDomain, hasWebsite, websiteIsBuilderOrSocial`. With `maxReviewsPerTrade > 0`: `reviews[]` with `source: "api"`.

**Scales:** ratings are **out of 10**. `rating` = recent mean; `reviewMetrics.totalScore` = all-time.

**Billing:** $0.005 per run start + $0.004 per tradesperson row + $0.05 per contact email actually found (`enrichEmails` only, off by default). Reviews, pagination and profile fetches are not billed separately.

**Agent-relevant behaviour:** no login and no browser; `companyId` is the dedup key; a search caps at 60 pages; free-tier runs cap at 100 rows; the run trims `maxItems` to what the charge limit can pay for; if every request is blocked the run FAILS with the block count rather than returning an empty success.

**Do not assume:** an email on every row (measured ~0 on a 7-row test), a website on every row (~1 in 3, and ~30% of those are dead), a 5-point rating scale, or prose in `fullDescription` (some profiles return an internal reference like `"$38"` — fall back to `summary`).

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by Checkatrade or any of its subsidiaries or affiliates. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available Checkatrade search-listing pages, trade-profile pages and the public consumer reviews API — no authenticated endpoints, no login-only or private data. Users are responsible for ensuring their use complies with Checkatrade's Terms of Service, applicable data-protection law (including UK GDPR) and their own organisation's contractual obligations. Use personal data such as tradesperson names, phone numbers and reviewer names lawfully, respect rate limits, honour opt-out and do-not-contact requirements, and do not republish scraped review content in violation of copyright.

No scraped data is stored by the Actor's author — it is returned directly to your own Apify dataset.

***

### SEO Keywords

checkatrade scraper, scrape checkatrade, checkatrade data extraction, checkatrade api, checkatrade reviews scraper, tradesperson scraper, uk trades directory scraper, checkatrade phone number scraper, builder scraper uk, electrician scraper, plumber scraper uk, local trades data, uk tradesmen data, checkatrade listings scraper, checkatrade profiles, checkatrade search scraper, trade reviews dataset, uk contractor data, home services data, apify checkatrade, trades lead generation, local business directory scraper

# Actor input Schema

## `trade` (type: `string`):

The trade to search for. Any Checkatrade category works, e.g. `Plumber`, `Builder`, `Electrician`, `Gas Boiler Servicing Repair`. Combined with each location below into a `/Search/{Trade}/in/{Location}` URL, which is then paginated. Multi-word trades are Title-Cased and dash-joined automatically. No default — leave empty if you are pasting URLs instead. Example: `Plumber`.

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

Towns, cities or areas to search in — any place Checkatrade supports, e.g. `London`, `Manchester`, `Birmingham`, `Milton Keynes`. Each entry is combined with the trade above into its own search and paginated. Multi-word places are Title-Cased and dash-joined automatically. Leave empty with a trade set to run one UK-wide search. Example: `["London", "Leeds"]`.

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

Checkatrade URLs to scrape, auto-classified into two kinds: search listings (`https://www.checkatrade.com/Search/Builder/in/London`) which are paginated, and trade profiles (`https://www.checkatrade.com/trades/{slug}`) which each produce one fully detailed row. Mixable with the trade + locations above in the same run. Anything that is not a checkatrade.com search or profile URL is logged and skipped. Default: empty.

## `scrapeDetails` (type: `boolean`):

When true, each tradesperson found in a search also gets their profile page fetched, adding the full description, the complete skill/sub-skill list, services offered, per-dimension review metrics, up to 6 recent reviews and — where the tradesperson published one — their own website (present on roughly 1 profile in 3). Costs about 2× the requests. When false you still get the ~30 listing-card fields per tradesperson. Profile URLs you paste in startUrls are always fully detailed regardless. Default: false.

## `maxReviewsPerTrade` (type: `integer`):

How many customer reviews to collect per tradesperson. `0` (the default) = only the up-to-6 recent reviews embedded in the profile page, which needs 'Scrape full profile pages' above. Any value above 0 pulls that many from Checkatrade's public reviews API — full history with per-dimension scores (workmanship, reliability, communication), reviewer name, work category, price band, the trade's reply and review photos. The API needs only the company ID that listing rows already carry, so this works with profile scraping off and adds no profile fetches. Costs 1 request per 25 reviews (e.g. 400 reviews = 16 requests) but no extra charge. Example: `100`. Max 1000.

## `enrichEmails` (type: `boolean`):

Opt-in, best-effort contact-email discovery. It seeds from the tradesperson's own website when their profile exposes one — measured live that is only 25–35% of profiles, and ~30% of those domains are dead, so the realistic seed rate is ~17%. In a 7-row test this returned 0 emails. Adds contactEmail + contactWebsite columns plus a detailed emailEnrichment object when something is found. Billed only on an actual find ($0.05 each); no email, no charge. Treat it as an occasional bonus, not a lead source. Default: false.

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

Hard cap on the number of tradesperson rows across the entire run, counting every search and URL. Each row is one paid `tradesperson` event. The run also trims itself down to whatever your maximum-cost-per-run setting can actually pay for. Free-tier Apify accounts are additionally capped at 100 rows. Example: `500`. Default: 1000.

## `maxItemsPerSearch` (type: `integer`):

Cap on rows produced by each individual trade+location search, applied before the whole-run cap above. Checkatrade returns ~12 tradespeople per page and this actor follows at most 60 pages per search (~700 rows), so lower this to sample many towns evenly instead of exhausting the first one. Example: `50`. Default: 1000.

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

How many HTTP requests run in parallel. Checkatrade is protected by Cloudflare, so keep this moderate — 4 to 8 is the sweet spot; pushing higher gets more challenges, not more speed. Example: `6`. Default: 6. Max 15.

## `maxRequestRetries` (type: `integer`):

Retry budget per request for Cloudflare challenges (403), rate limits (429), 5xx and network errors. Each retry rotates to a fresh GB residential IP with a short backoff, which is what gets a blocked page through. The challenge is flaky, so a generous budget helps. Example: `15`. Default: 15.

## `proxy` (type: `object`):

Leave this empty. The actor already routes every page request through its own GB residential pool at no extra cost to you — which is what gets past Checkatrade's Cloudflare challenge. Set it only if you must force your own proxies; a non-UK or datacenter pool will get challenged and return fewer rows. Default: unset (built-in GB residential).

## Actor input object example

```json
{
  "trade": "Builder",
  "locations": [
    "London"
  ],
  "startUrls": [],
  "scrapeDetails": false,
  "maxReviewsPerTrade": 0,
  "enrichEmails": false,
  "maxItems": 1000,
  "maxItemsPerSearch": 1000,
  "maxConcurrency": 6,
  "maxRequestRetries": 15
}
```

# 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 = {
    "trade": "Builder",
    "locations": [
        "London"
    ],
    "startUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/checkatrade-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 = {
    "trade": "Builder",
    "locations": ["London"],
    "startUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/checkatrade-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 '{
  "trade": "Builder",
  "locations": [
    "London"
  ],
  "startUrls": []
}' |
apify call memo23/checkatrade-scraper --silent --output-dataset

```

## MCP server setup

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