# Local Business Leads — Verified Emails & Contacts (`commonelements/local-business-leads`) Actor

Local lead generation that crawls SMB websites and returns verified business emails, phones and socials. Bring a niche + city (optional Maps discovery) or your own site list. Honest email verification: never marks an email 'deliverable' when it's catch-all or unknown. Outreach-ready leads.

- **URL**: https://apify.com/commonelements/local-business-leads.md
- **Developed by:** [Harry Schoeller](https://apify.com/commonelements) (community)
- **Categories:** Lead generation, Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 96.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 dataset item scrapeds

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

## Local Business Leads — Verified Emails & Contacts

**Local lead generation that actually stays working.** Give this Actor a niche + city (or your own list of business websites) and it returns **outreach-ready leads**: a verified business **email**, the contact name, **phone**, and social profiles for each company — one clean row per business.

It is a focused **business email finder** and **local lead generation** engine built around one durable idea: a plumber's, dentist's, or law firm's **own website** has no anti-bot defense, so crawling it for contact details is cheap, fast, and reliable for years. Every email then runs through honest, in-process **email verification** (syntax + MX + disposable/role/catch-all + score) — so you get **verified emails**, not a raw dump.

Keywords: business leads, email finder, local lead generation, verified emails, B2B leads, contact scraper, lead enrichment, small business email list, sales prospecting.

***

### Why this Actor (the honesty wedge)

Most local-leads scrapers dump raw Google Maps rows and leave you to guess which emails are real. This Actor does the opposite:

- **Verified, not guessed.** Each email gets a status (`deliverable` / `risky` / `undeliverable` / `unknown`) and a transparent **0–100 score** with reasons. An email is labeled **`deliverable` only when every reliable signal is green**. Catch-all domains and SMTP-blocked checks are honestly marked **`risky`** or **`unknown`** — **never** laundered into `deliverable`.
- **Real contacts, never fabricated.** Every email, phone and social URL is extracted **verbatim** from the business's own site (`mailto:` links, JSON-LD, Cloudflare-obfuscated addresses, visible text). Nothing is hallucinated.
- **Role mailboxes kept, not dropped.** For small businesses, `info@` / `office@` is frequently the *only* reachable address. We keep it and label it (`primaryEmailType: "role"`, downgraded to `risky`) so **you** decide. Honest beats empty.

***

### Three ways to feed it (all converge on the same reliable core)

You can use any one — or combine them.

#### 1. Bring your own websites (default, zero extra cost)

```json
{
  "websites": ["acme-plumbing.com", "https://brightsmiledental.com"]
}
```

Bare domains or full URLs both work.

#### 2. Chain off an existing dataset (Maps scraper output, a CRM export, etc.)

```json
{
  "sourceDatasetId": "<DATASET_ID>",
  "websiteField": "website",
  "passThroughFields": ["categoryName", "totalScore", "address"]
}
```

This lets the Actor sit **downstream of any Google Maps scraper** and **upstream of email-verifier** or a CRM importer.

#### 3. Discover by category + location (optional Google Maps discovery)

```json
{
  "categorySearch": { "category": "dentist", "location": "Austin, TX", "maxPlaces": 50 },
  "enableMapsDiscovery": true
}
```

When `enableMapsDiscovery` is **on**, the Actor calls a maintained Google Maps scraper (`mapsActorId`, default `compass/crawler-google-places`) via `Actor.call`, then enriches + verifies its results.

> ⚠️ **Cost disclosure:** Maps discovery runs a **separate upstream Actor** that **bills your Apify account independently** at its own rate. This Actor depends only on that scraper's dataset shape (name, address, phone, website) — never on Google Maps internals — so if a provider degrades you can swap `mapsActorId` in one line, and a Maps outage simply degrades to "bring your own list" instead of breaking the product.

***

### Output: one outreach-ready row per business

```jsonc
{
  "businessName": "Bright Smile Dental",
  "category": "dentist",
  "website": "https://brightsmile.com",
  "domain": "brightsmile.com",
  "address": "123 Main St, Austin, TX",
  "location": "Austin, TX",

  "primaryEmail": "dr.lee@brightsmile.com",
  "primaryEmailStatus": "deliverable",      // deliverable | risky | undeliverable | unknown
  "primaryEmailScore": 92,                   // 0–100, honest
  "primaryEmailType": "personal",            // personal | role | unknown
  "contactName": "Dr. Susan Lee",            // best-effort, null if not found
  "phone": "+1-512-555-0100",

  "emails": [
    { "email": "dr.lee@brightsmile.com", "status": "deliverable", "score": 92,
      "isRole": false, "isCatchAll": false, "isDisposable": false,
      "isFreeProvider": false, "source": "mailto", "reasons": ["valid_syntax", "mx_found:..."] }
  ],
  "phones": ["+1-512-555-0100"],
  "socials": { "facebook": "https://facebook.com/...", "instagram": null,
               "linkedin": null, "twitter": null, "youtube": null, "tiktok": null },

  "leadQuality": "high",                     // high | medium | low
  "emailFound": true,
  "verifiedDeliverable": true,
  "websiteReachable": true,
  "pagesCrawled": 3,
  "discoverySource": "websites_input",       // maps_call | websites_input | source_dataset
  "crawledAt": "2026-06-20T18:00:00Z",
  "passThrough": { "totalScore": 4.6 }
}
```

#### `leadQuality` rubric (deterministic)

- **high** — personal email that is `deliverable`/`risky` **and** a phone is present.
- **medium** — a role email that is `deliverable`/`risky`, OR a personal email that is `unknown`, OR a usable personal email with no phone.
- **low** — email `undeliverable`, or no email found (still emitted with `emailFound:false` unless `requireDeliverable` is on).

A run-level `OUTPUT` summary records `{ totalBusinesses, withEmail, deliverable, risky, unknown, undeliverable, sitesUnreachable, discoverySource, disclaimer }`.

***

### How it works

1. **Discover** → resolve websites from `websites[]`, a `sourceDatasetId`, and/or optional Maps discovery; dedup by domain so each business is crawled once.
2. **Crawl** → a fast HTTP (Cheerio) crawl of each site: the homepage plus only the same-domain pages whose path/anchor text signals contact intent (`/contact`, `/about`, `/team`, `impressum`, …), capped by `maxPagesPerSite`. A polite, focused crawl — not a full site walk. Set `renderJs: true` only for the rare JS-only site.
3. **Extract** → emails (`mailto:` › JSON-LD › Cloudflare cfemail › visible-text regex), phones (`tel:` › JSON-LD › strict regex), socials (profile URLs, tracking pixels filtered out), and a best-effort contact name.
4. **Dedup & rank** → per business: rank emails by source confidence and personal-over-role; globally: flag emails shared across businesses.
5. **Verify (in-process)** → `syntax → MX/DNS → catch-all → optional best-effort SMTP → honest score`. Same engine and honesty contract as the **Email Verifier** Actor, copied in-process so verification adds **no second run**.

***

### Chaining with Email Verifier

This Actor verifies emails itself, so a single run gives you verified leads. For a deeper re-scoring pass, its output chains **zero-glue** into the **Email Verifier** Actor:

- Set the verifier's `sourceDatasetId` to this Actor's default dataset ID,
- `emailField: "primaryEmail"`,
- `passThroughFields: ["businessName", "website", "phone"]`.

The shared `passThroughFields` convention means **Maps scraper → Local Business Leads → Email Verifier** forms a clean pipeline where each link is independently reliable.

***

### Pricing (pay-per-event)

| Event | When | Price |
|---|---|---|
| **Lead enriched & verified** | Once per unique business after dedup (site crawled + contacts extracted + emails verified) | **$0.003** (= $3 / 1,000 leads) |
| **Best-effort SMTP probe** | Only when `smtpCheck = best_effort` **and** a probe is actually attempted | $0.002 |

Charging stops **gracefully** at your PPE budget cap (`budgetStopped: true`) — it never overcharges and never crashes. When `enableMapsDiscovery` is on, the upstream Maps Actor bills you **separately**.

***

### Key inputs

| Input | Default | Notes |
|---|---|---|
| `websites` | – | Business sites or bare domains. |
| `categorySearch` + `enableMapsDiscovery` | off | Optional Maps discovery (upstream Actor billed separately). |
| `mapsActorId` | `compass/crawler-google-places` | Swappable upstream provider. |
| `sourceDatasetId` / `websiteField` | – / `website` | Chain off another dataset. |
| `passThroughFields` | `[]` | Carry source fields onto each lead. |
| `maxPagesPerSite` | `5` | Focused crawl budget per business. |
| `renderJs` | `false` | Playwright for JS-only sites (slower). |
| `smtpCheck` | `off` | `best_effort` may downgrade / mark `unknown` — never promotes. |
| `detectCatchAll` | `true` | Catch-all domains capped at `risky`. |
| `treatRoleAsRisky` | `true` | Role mailboxes kept + flagged. |
| `requireDeliverable` | `false` | Emit only leads with a `deliverable`/`risky` email. |
| `maxLeads` | `0` | `0` = unlimited. |
| `proxyConfiguration` | Apify proxy | Datacenter proxy is fine for SMB sites. |

***

### Honest limitations

- We extract only what a site **publishes**. A business that hides its email behind a JS contact form may yield no email (`emailFound: false`) — we say so rather than invent one.
- SMTP from datacenter IPs is unreliable (port 25 is often blocked). `best_effort` SMTP can only **downgrade** confidence or return `unknown`; it will **never** mark an email `deliverable`.
- Catch-all domains accept every address, so per-mailbox deliverability is unknowable — those are honestly capped at `risky`.

# Actor input Schema

## `websites` (type: `array`):

List of business websites or bare domains to enrich (e.g. acme.com or https://acme.com). We crawl each site for contact emails, phones and socials, then verify the emails. Use this OR a source dataset OR Maps discovery (you can combine them).

## `categorySearch` (type: `object`):

OPTIONAL discovery: find businesses by niche + city via a maintained Google Maps scraper, then enrich them. Requires enableMapsDiscovery=true. The upstream Maps actor bills you separately. Example: { "category": "dentist", "location": "Austin, TX", "maxPlaces": 50 }.

## `enableMapsDiscovery` (type: `boolean`):

Must be explicitly turned on to use categorySearch. When on, this actor calls a maintained Google Maps scraper (mapsActorId) via Actor.call — that upstream actor incurs its own separate metered cost on your account.

## `mapsActorId` (type: `string`):

The maintained upstream Google Maps scraper to call for discovery. Swap in one line if a provider degrades.

## `sourceDatasetId` (type: `string`):

Enrich businesses from another actor's dataset (e.g. a Google Maps scraper or any leads list). Provide this + websiteField. Lets this actor sit downstream of a Maps scraper and upstream of email-verifier / a CRM importer.

## `websiteField` (type: `string`):

Field in the source dataset that holds each business's website URL. We also try url, domain, website automatically. Supports dot paths.

## `passThroughFields` (type: `array`):

Fields from the source dataset / Maps to copy onto each lead (e.g. categoryName, totalScore, address, reviewsCount). Supports dot paths.

## `maxPagesPerSite` (type: `integer`):

Crawl budget per business: the homepage plus contact/about/team pages. Higher finds more contacts but costs more crawl time.

## `renderJs` (type: `boolean`):

Off by default (fast, cheap, reliable HTTP/Cheerio crawl). Turn on only for the rare JS-only site that hides contact info behind client-side rendering.

## `smtpCheck` (type: `string`):

Off (recommended), or best-effort. SMTP from datacenter IPs is unreliable: it can only DOWNGRADE confidence or return 'unknown' — it will NEVER mark an email 'deliverable'.

## `detectCatchAll` (type: `boolean`):

Probe each unique email domain once to detect accept-all (catch-all) configuration. Catch-all means per-mailbox deliverability is unknowable; such emails are capped at 'risky'.

## `treatRoleAsRisky` (type: `boolean`):

Flag info@/office@/sales@ etc. as risky. Note: for SMB leads a role mailbox is often the ONLY contact — we keep it and label it, never silently drop it.

## `requireDeliverable` (type: `boolean`):

If on, emit only leads whose best email is 'deliverable' or 'risky' (drops undeliverable / no-email leads). Off by default — honest > empty.

## `deduplicate` (type: `boolean`):

Collapse the same business (same domain) appearing across multiple sources into one charged lead.

## `maxLeads` (type: `integer`):

Maximum number of leads to emit. 0 = unlimited.

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

Apify proxy for crawling business sites. Datacenter proxy is fine for SMB sites; residential is not needed.

## Actor input object example

```json
{
  "websites": [
    "https://www.bartonhillstx.com",
    "https://www.juliemathisdds.com"
  ],
  "categorySearch": {
    "category": "dentist",
    "location": "Austin, TX",
    "maxPlaces": 25
  },
  "enableMapsDiscovery": false,
  "mapsActorId": "compass/crawler-google-places",
  "websiteField": "website",
  "passThroughFields": [
    "categoryName",
    "totalScore",
    "address"
  ],
  "maxPagesPerSite": 5,
  "renderJs": false,
  "smtpCheck": "off",
  "detectCatchAll": true,
  "treatRoleAsRisky": true,
  "requireDeliverable": false,
  "deduplicate": true,
  "maxLeads": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `leads` (type: `string`):

No description

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

No description

# 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 = {
    "websites": [
        "https://www.bartonhillstx.com",
        "https://www.juliemathisdds.com"
    ],
    "categorySearch": {
        "category": "dentist",
        "location": "Austin, TX",
        "maxPlaces": 25
    },
    "passThroughFields": [
        "categoryName",
        "totalScore",
        "address"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("commonelements/local-business-leads").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 = {
    "websites": [
        "https://www.bartonhillstx.com",
        "https://www.juliemathisdds.com",
    ],
    "categorySearch": {
        "category": "dentist",
        "location": "Austin, TX",
        "maxPlaces": 25,
    },
    "passThroughFields": [
        "categoryName",
        "totalScore",
        "address",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("commonelements/local-business-leads").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "websites": [
    "https://www.bartonhillstx.com",
    "https://www.juliemathisdds.com"
  ],
  "categorySearch": {
    "category": "dentist",
    "location": "Austin, TX",
    "maxPlaces": 25
  },
  "passThroughFields": [
    "categoryName",
    "totalScore",
    "address"
  ]
}' |
apify call commonelements/local-business-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=commonelements/local-business-leads",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/DxwABnmyBJYcN7Gri/builds/4tScfzCvX2wNee5Ji/openapi.json
