# Plumbing Lead Scraper - Maps Website Checker (Unofficial) (`brainy_frostfield/plumbing-lead-scraper`) Actor

UNOFFICIAL Google Maps plumbing lead scraper with website health scores, tech detection, and HOT/WARM/COLD ranking. Not affiliated with Google. No AI language models.

- **URL**: https://apify.com/brainy\_frostfield/plumbing-lead-scraper.md
- **Developed by:** [Viv K](https://apify.com/brainy_frostfield) (community)
- **Categories:** Lead generation, Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 place leads

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Plumbing Lead Scraper – Google Maps + Website Checker (Unofficial)

Find **plumbers and related local businesses** from Google Maps, then run **deterministic website health checks** (reachability, SSL, SEO basics, tech stack, contact signals) and rank leads **HOT / WARM / COLD**.

Built for web design, SEO, and home-services agencies that sell sites, redesigns, or local SEO to plumbers.

No AI language models. No Google Places API key required.

> **UNOFFICIAL** — not affiliated with, endorsed by, or sponsored by Google LLC. Google Maps is a trademark of Google LLC.\
> You are solely responsible for complying with Google’s Terms of Service, privacy laws (GDPR/CCPA), anti-spam rules (CAN-SPAM/CASL), website terms, and [Apify’s Acceptable Use Policy](https://docs.apify.com/legal/acceptable-use-policy).\
> **Do not use for unsolicited mass messaging.** Scraping Google Maps may violate Google’s contractual terms even when data is publicly visible.

***

### Features

- Search Google Maps by **location** and **keywords** (plumbers, drain cleaning, emergency plumber…)
- Extract **name, address, phone, website, rating, hours, Maps URL**
- Detect **unclaimed** Google listings
- Website health: **HTTP status, HTTPS, SSL, response time, title/meta, mobile viewport, forms, phones, emails, schema, analytics, socials, platform** (WordPress / Wix / GoDaddy…)
- **Website health score** 0–100 with issues / warnings / positives
- **Lead score** 0–100 and tier **HOT / WARM / COLD** (no website / down / weak site = opportunity)
- Optional public **emails** with free **MX domain check**
- Phone normalization with **libphonenumber**
- Simple **niche tag** (e.g. emergency plumbing, water heater, drain cleaning)

Health scores and contact data are rule-based from public pages only. Nothing is invented by AI.

***

### How to use

1. Add one or more **locations** (city or ZIP).
2. Add **search terms** (e.g. `plumbers`).
3. Set **Max results per query** (try **10–20** first; defaults stay small for Store tests).
4. Keep **Check websites** on for health scores (and optional contact-page probes).
5. Set proxy to **Apify Proxy → RESIDENTIAL** (recommended for Maps).
6. Click **Start**, then download the dataset.

#### Example input

```json
{
  "locations": ["Houston, TX"],
  "searchTerms": ["plumbers"],
  "maxPlacesPerQuery": 10,
  "checkWebsites": true,
  "checkContactPage": true,
  "validateMxRecords": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

#### Input fields

| Field | Description |
|---|---|
| Target locations | Cities, states, or ZIPs |
| Search terms | Maps keywords |
| Max results per query | Cap per location × term |
| Max total places | Optional cap for the whole run |
| Check websites | Run HTTPS/SSL/SEO/tech/contact health checks |
| Probe contact pages | Try common `/contact` paths |
| Validate MX | Free check that email domains can receive mail |
| Website request timeout | Seconds per site request |
| Proxy for Google Maps | Prefer **residential** |
| Filters | Phone only / with website / reachable only / needs-website only / include no website |

***

### Output example

```json
{
  "query": "plumbers in Houston, TX",
  "businessName": "Houston Flow Plumbing",
  "niche": "Emergency plumbing",
  "address": "500 Main St, Houston, TX 77002",
  "primaryPhone": "+17135550100",
  "primaryPhoneNational": "(713) 555-0100",
  "hasPhone": true,
  "website": "https://houstonflowplumbing.example",
  "hasWebsite": true,
  "websiteHealthScore": 72,
  "needsWebsite": false,
  "websiteCheck": {
    "reachable": true,
    "httpStatus": 200,
    "sslValid": true,
    "hasMobileViewport": true,
    "hasContactForm": true,
    "detectedPlatform": "WordPress",
    "websiteIssues": [],
    "websiteWarnings": ["Missing LocalBusiness schema"]
  },
  "primaryEmail": "office@houstonflowplumbing.example",
  "emails": [
    {
      "email": "office@houstonflowplumbing.example",
      "source": "mailto",
      "confidence": 95,
      "mxValid": true
    }
  ],
  "leadScore": 55,
  "leadTier": "COLD",
  "leadReasons": [],
  "googleMapsUrl": "https://www.google.com/maps/place/...",
  "scrapedAt": "2026-08-08T12:00:00.000Z"
}
```

***

### Pricing

**Pay per event** + **platform usage**. You pay **both**:

1. **Event fees** (below) — paid to the Actor developer
2. **Platform usage** — paid to Apify for compute, data transfer, storage, and **proxies**

#### Platform usage & residential proxy (important)

Google Maps runs need **Apify Proxy → RESIDENTIAL**. Residential traffic is billed by Apify **per GB** (rate depends on your plan; often around **$7–$8 / GB** on lower plans).

- Browser/Maps traffic is heavier than simple HTTP scrapes, so even small runs can use tens–hundreds of MB.
- **Proxy + compute usually cost more than the event fees** on short test runs.
- Website health checks add some compute, but **Maps residential proxy** is typically the largest usage line.
- Start with a **small test** (low `maxPlacesPerQuery`) before large city lists.
- Without residential proxy you often get **0 results**, but you still avoid that proxy charge.

#### Event fees

| Event | When charged | Price |
|---|---|---|
| Place lead | Each business saved | **$0.004** ($4.00 per 1,000) |
| Phone enrichment | Only if at least one validated phone is found | **$0.005** ($5.00 per 1,000) |
| Website health check | When a listed website was health-checked | **$0.006** ($6.00 per 1,000) |
| No-website opportunity | When the Maps listing has no website | **$0.008** ($8.00 per 1,000) |

**Event fee examples** (platform usage — including residential proxy — is **extra**):

| Result mix | Approx. event cost only |
|---|---|
| 1,000 places, all with phone + website checked | **$15.00** + usage |
| 1,000 places with no website (all opportunity) | **$17.00** + usage |
| 1,000 places, ~90% phone, ~80% website checked, ~10% no website | **~$14.30** + usage |

Total cost scales with locations, search terms, max results, **residential proxy GB**, and compute. Defaults stay small for Store auto-tests and cheap first runs.

***

### Tips

- **Residential proxy** is important for Google Maps. Without it you may get zero results.
- Expect **proxy usage** to be a large share of the run cost; keep max results low while testing.
- More cities × more keywords × higher max results = longer runs and higher **proxy + compute** cost.
- **No website**, **down site**, **Wix/Weebly/GoDaddy**, or **unclaimed** listings often score **HOT** for agencies.
- Use the **HOT / website opportunities** dataset view to prioritize outreach.

***

### Compliance

- Public business information and public website pages only; no login walls.
- Business emails and phones may still be personal data under GDPR/CCPA in some cases.
- You are responsible for lawful use and outreach (anti-spam and privacy rules).
- Do not use for spam or illegal activity.

***

### Limitations

- Google Maps may block non-residential traffic.
- Some sites block bots (403/429) — reported as blocked/unreachable.
- Content in images or heavy JS-only apps may be incomplete.
- Health score is a heuristic, not a conversion guarantee.
- Maps does not return unlimited listings per area.
- Maps page layout can change over time.

***

### Troubleshooting

| Problem | What to try |
|---|---|
| 0 results | Use residential proxy; lower max results; check the run log |
| Sites marked unreachable | Site may block bots; try again later or accept as opportunity signal |
| Slow or costly run | Fewer locations/terms; lower max results; reduce contact-page probes |
| Empty after filters | Turn off “only reachable” / “only needs website” for testing |

Support: open an **Issue** on this Actor with your run ID and input (no secrets).

# Actor input Schema

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

Cities, states, or ZIP codes (e.g. Houston, TX).

## `searchTerms` (type: `array`):

Maps keywords (plumbers, drain cleaning, emergency plumber…).

## `maxPlacesPerQuery` (type: `integer`):

Maximum businesses per location × term. Default is small for fast Store tests; raise for production lists.

## `maxTotalPlaces` (type: `integer`):

Optional run-wide cap. Leave empty/0 for no global cap.

## `checkWebsites` (type: `boolean`):

Run HTTPS/SSL/SEO/tech/contact health checks on each listed website.

## `checkContactPage` (type: `boolean`):

Also try common /contact paths for forms, phones, and emails.

## `requestTimeoutSecs` (type: `integer`):

Timeout per website request.

## `websiteRequestDelayMs` (type: `integer`):

Polite delay between website health checks.

## `validateMxRecords` (type: `boolean`):

Free DNS check that email domains can receive mail.

## `defaultCountryCode` (type: `string`):

ISO country code used when parsing phone numbers.

## `includeNoWebsite` (type: `boolean`):

Keep listings with no website (often HOT for web agencies).

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

Only keep leads that have at least one validated phone.

## `onlyWithWebsite` (type: `boolean`):

Only keep leads that list a website on Maps.

## `onlyReachableWebsite` (type: `boolean`):

Only keep leads whose website returned a successful HTTP response.

## `onlyNeedsWebsite` (type: `boolean`):

Only keep HOT / needs-website opportunities (no site, down, or very weak).

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

Residential proxy is strongly recommended for Maps.

## `maxScrolls` (type: `integer`):

How many times to scroll Maps results.

## `debugLog` (type: `boolean`):

More detail in the run log.

## Actor input object example

```json
{
  "locations": [
    "Houston, TX"
  ],
  "searchTerms": [
    "plumbers"
  ],
  "maxPlacesPerQuery": 5,
  "checkWebsites": true,
  "checkContactPage": true,
  "requestTimeoutSecs": 25,
  "websiteRequestDelayMs": 600,
  "validateMxRecords": true,
  "defaultCountryCode": "US",
  "includeNoWebsite": true,
  "onlyWithPhone": false,
  "onlyWithWebsite": false,
  "onlyReachableWebsite": false,
  "onlyNeedsWebsite": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxScrolls": 10,
  "debugLog": false
}
```

# Actor output Schema

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

Dataset of plumbing leads with website checks.

# 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 = {
    "locations": [
        "Houston, TX"
    ],
    "searchTerms": [
        "plumbers"
    ],
    "maxPlacesPerQuery": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("brainy_frostfield/plumbing-lead-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 = {
    "locations": ["Houston, TX"],
    "searchTerms": ["plumbers"],
    "maxPlacesPerQuery": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("brainy_frostfield/plumbing-lead-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 '{
  "locations": [
    "Houston, TX"
  ],
  "searchTerms": [
    "plumbers"
  ],
  "maxPlacesPerQuery": 5
}' |
apify call brainy_frostfield/plumbing-lead-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,brainy_frostfield/plumbing-lead-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/wN9zRXus6RyBiFrxY/builds/kgayWrRTPCmQMNial/openapi.json
