# Local Business Phone Scraper (`lightmoon/local-business-phone-scraper`) Actor

Phone numbers, addresses, websites, emails and opening hours for local businesses, by trade and city. Twenty-five columns on every row, including the Better Business Bureau letter grade, review counts and coordinates. Rows with no contact are free, and so are emails we fail to find.

- **URL**: https://apify.com/lightmoon/local-business-phone-scraper.md
- **Developed by:** [Stable](https://apify.com/lightmoon) (community)
- **Categories:** Lead generation, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.99 / 1,000 contacts

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/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

Give it a trade and a city — `plumber`, `Chicago, IL` — and get back local
businesses with phone, address, website, opening hours, category and a Better
Business Bureau letter grade. Twenty-five columns on every row.

**You are billed only for rows that carry a phone number or a website, and only
for emails that were actually found.** A row with no way to reach the business
is free. A website that turns out to have no email address is free.

The last verification run returned 108 Chicago plumbers and dentists in 43
seconds — every one with a phone number, 17 of them with an email as well.

### What it costs

| Your Apify plan | Per 1,000 businesses with a contact | Per 1,000 emails found |
|---|---|---|
| Free | $9.99 | $5.00 |
| Starter | $9.99 | $5.00 |
| Scale | $6.50 | $3.20 |
| Business and above | **$3.99** | **$2.00** |

Plus $0.00005 to start a run — five thousandths of a cent. No per-page fee, no
proxy cost, no charge for a search that finds nothing.

The email pass is **off by default** and costs nothing until you switch it on.
When it is on, only a found address is billed: on the measured runs about four
websites in ten yield one, and the other six cost you nothing at all. Compare
that with paying per row and receiving an email on some unstated fraction of
them.

***

### What one row looks like

Real output, `plumber` in `Chicago, IL`, with the email pass on:

```json
{
  "name": "Power Plumbing & Sewer Contractor, Inc.",
  "phone": "+17732489574",
  "additionalPhones": "",
  "email": "power@powerplumbinginc.com",
  "emailsFound": "power@powerplumbinginc.com,service@powerplumbinginc.com",
  "website": "https://powerplumbinginc.com",
  "socialProfiles": "https://facebook.com/PowerPlumbingChicago,https://linkedin.com/company/power-plumbing",
  "address": "3840 N Ashland Ave",
  "city": "Chicago",
  "state": "IL",
  "postalCode": "60613-2708",
  "country": "US",
  "latitude": "41.95162582397461",
  "longitude": "-87.66901397705078",
  "category": "Plumber",
  "rating": "A+",
  "reviewCount": 218,
  "openingHours": "Mon 24h; Tue 24h; Wed 24h; Thu 24h; Fri 24h; Sat 24h; Sun 24h",
  "accredited": true,
  "sourceName": "bbb,duckduckgo",
  "sourceUrl": "https://www.bbb.org/us/il/chicago/profile/plumber/power-plumbing-sewer-contractor",
  "sourceId": "1001869",
  "searchTerm": "plumber",
  "searchLocation": "Chicago, IL"
}
```

### The 25 columns

| Group | Columns |
|---|---|
| **Contact** | `phone`, `additionalPhones`, `email`, `emailsFound`, `website`, `socialProfiles` |
| **Identity** | `name`, `category`, `recordType` |
| **Location** | `address`, `city`, `state`, `postalCode`, `country`, `latitude`, `longitude` |
| **Trust** | `rating` (A+ to F, or a star score), `reviewCount`, `accredited` |
| **Operations** | `openingHours` — the full week, with `24h` where a business never closes |
| **Provenance** | `sourceName`, `sourceUrl`, `sourceId`, `searchTerm`, `searchLocation` |

Every row carries all 25 in the same order, so the CSV has no ragged edges. A
field the source did not publish comes back **empty — never guessed, never
filled with `N/A`**.

`email` is the single best address; `emailsFound` is every address on the page,
in case you want them. The best one prefers the business's own domain over a
free mailbox, and a role box like `info@` over a named person — `info@` is
answered by whoever is on duty, `john@` bounces the day John leaves.

### Field coverage, measured

Over **1,821 businesses**: 11 trades — plumber, dentist, HVAC, roofing,
electrician, law firm, landscaping, pest control, locksmith, moving, accountant
— across Chicago, Austin, Phoenix, Denver, Miami, Seattle, Atlanta and Boston.

| | |
|---|---|
| **A phone number or a website** | **100%** (1,821 of 1,821) |
| A phone number | 99.9% |
| `name`, `city`, `country`, `latitude`, `longitude` | 100% |
| `state`, `postalCode` | 99.9% |
| `category` | 98.1% |
| `address` (the street line) | 92.6% |
| `rating` | 92.4% |
| `accredited` | 52.3% |
| `website` | 50.4% |
| `openingHours` | 48.5% |
| `reviewCount` | 43.2% |
| **`email`, of the websites actually read** | **~42%** |
| `socialProfiles`, of the websites read | ~65% |

Every trade and every city in that sample returned a contact on 100% of rows.
The businesses missing a street line are overwhelmingly home-based contractors
who publish a phone and a service area but no premises.

**Why some columns are around half.** Two directories are searched, and they
return largely *different* businesses rather than the same ones twice —
measured overlap is **2.8%**. So a row usually comes from one of them: a Better
Business Bureau row carries the letter grade and the accreditation flag; a
places-index row carries the website, the hours and the review count. Name,
phone, address, city, state, postcode and coordinates come from both. Where a
business appears in both, the row is merged and has everything, as in the
sample above.

Because the email pass reads the *website*, it can only run on rows that have
one — about four in ten. Select one source if you would rather have consistent
columns than maximum coverage.

### Input

```json
{
  "searchTerms": ["plumber", "hvac contractor"],
  "locations": ["Chicago, IL", "Austin, TX"],
  "sources": ["bbb", "duckduckgo"],
  "maxResultsPerSearch": 100,
  "findEmails": true,
  "onlyWithContact": false
}
```

Terms are crossed with locations, so two terms and two cities is four searches.

**A location is required, and it has to be a real place.** Results are
geolocated from the address the request comes from, so a search with no city in
it would return businesses near the server rather than near you. The Actor
refuses that input instead of handing you a tidy table of the wrong city.

### What this does not return

- **No personal contact details.** Published business information only.
- **No verification that a number is in service or an inbox is monitored.**
  Addresses are read from the company's own website, not tested for delivery.
- **No more than about 246 businesses per search** — see below.
- **No email for a business with no website**, which is roughly six rows in ten.

### Limits, stated up front

- **Better Business Bureau returns at most 225 businesses per search.** Its own
  paging stops at 15 pages of 15, even on a query that reports 4,565 matches.
- **The places index returns at most 21 per search**, and has no paging at all.
- **So one search is not a whole city.** To cover a large market, list
  neighbourhoods as separate locations — `Lakeview, Chicago, IL`,
  `Pilsen, Chicago, IL`, `Wicker Park, Chicago, IL`. Measured: seven
  neighbourhood searches returned 67 distinct plumbers where one city-wide
  search returned 21.
- **United States and Canada are well covered. Other countries are thin.**
- Coverage is what the directories publish: good for trades and professional
  services, thinner for very new or very small businesses.

### Free plan

Runs on the Apify free plan, and the trial covers your first **100 results** —
enough to look at the data before paying for any of it. Every field is included
in the trial, the email pass included; nothing is held back for paying users.

Free-plan runs get less memory, so keep a first run to a handful of searches.

### Speed and cost of a run

108 businesses in 22 to 28 seconds over 8 requests, measured on the platform at
the default 1 GB. With the email pass on, the same run took 43 seconds and read
40 websites. Across the 1,821-business sample the directory half came to
**3.2 KB per business**, with no directory refusing a single request.

Requests to the directories are deliberately spaced — they throttle parallel
callers and forgive patient ones. Business websites are unrelated to each other
and are read several at a time, which is why the email pass costs seconds
rather than minutes. There is no proxy option because none is needed.

### Troubleshooting

**I asked for 500 and got 90.** You hit the per-search caps above. Split the
city into neighbourhoods, or add related trades — `plumber`, `emergency
plumber`, `drain cleaning`.

**`website` is empty on many rows.** Those rows came from the directory that
does not publish a website. The overlap between the two sources is about 2.8%,
so most rows carry one source's fields rather than both.

**`email` is empty on many rows.** Two reasons, and both are free to you: the
business has no website to read, or the website does not publish an address.
Turn on *Also open one contact page per site* to add about ten percentage
points.

**An email looks like a person, not the company.** The picker prefers role
boxes, but some businesses only publish a named address. Every address found on
the page is in `emailsFound` so you can choose differently.

**The run succeeded but returned very few rows.** The trade may be thin in that
town, or spelled differently there. Try the plain trade word before a long
phrase. Rows that came back with no contact were not charged for.

**A search in a non-US city returned little.** Expected — coverage outside the
United States and Canada is thin, and this page says so rather than letting you
find out on a bill.

### Output views

Five ready-made views: **Contacts** (name, phones, email, website, address,
grade), **Hours & reviews**, **Map data** (coordinates), **Outreach** (email,
socials, phone) and **All fields**. Export any of them to CSV, JSON or Excel,
or feed them straight into another Actor.

### Other scrapers we maintain

Same engine, same pricing model, same promise: flat rows, stated limits,
and no charge for a row that came back empty.

| | |
|---|---|
| **[Google Maps Contact Scraper](https://apify.com/lightmoon/google-maps-contact-scraper)** | Local businesses by category and city — phone, website, split address and opening hours. |
| **[YouTube Influencer Scraper](https://apify.com/lightmoon/youtube-influencer-scraper)** | Creators by niche, with the website and socials they publish and a subscriber filter. |
| **[Telegram Channel Messages Scraper](https://apify.com/lightmoon/telegram-messages-scraper)** | Posts from public channels — text, views, reactions and media links, no account needed. |
| **[Threads Hashtag Scraper](https://apify.com/lightmoon/threads-hashtag-scraper)** | Threads posts by hashtag or keyword, with author, engagement and links. |
| **[RedNote Xiaohongshu Profile Scraper](https://apify.com/lightmoon/rednote-profile-scraper)** | Public RedNote creator profiles in bulk — followers, bio, IP location and their notes. |
| **[ATS Jobs Scraper](https://apify.com/lightmoon/ats-jobs-scraper)** | Open roles straight from company career pages on Greenhouse, Lever, Ashby and Workday. |

# Actor input Schema

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

One trade or category per line — the words you would type into a directory. Each term is run against every location below, so three terms and four cities is twelve searches from two short lists.

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

One place per line, as `City, ST`. A location is required: results are geolocated by the address the request comes from, so a search with no place in it would return businesses near the server rather than near you. To cover a large city fully, list its neighbourhoods as separate lines — the directories cap how much any single search can return.

## `sources` (type: `array`):

Better Business Bureau carries the phone number, the A+ to F letter grade and the accreditation flag. DuckDuckGo adds the website, the opening hours and the review count. Both together give the fullest row; either alone still returns a phone number.

## `maxResultsPerSearch` (type: `integer`):

Caps each term-and-location pair. The directories themselves stop at roughly 225 from Better Business Bureau and 21 from DuckDuckGo per search, so a higher number simply returns everything available. Set 0 for no cap of your own.

## `maxResults` (type: `integer`):

A ceiling for the whole run, across every search. Set 0 for no cap.

## `onlyWithContact` (type: `boolean`):

Businesses without either are never charged for, whichever way this is set. Turn it on to keep them out of the export as well.

## `findEmails` (type: `boolean`):

Reads the website of each business that has one and pulls out the email address and any social profiles. Charged only when an email is actually found — a website with none costs nothing. Measured hit rate on US trade businesses: around a third of the sites tried, which is roughly half of all rows. It roughly doubles how long a run takes.

## `followContactPage` (type: `boolean`):

When the home page has no email, follow one link the site itself labels Contact or About. Adds about ten percentage points of hit rate and one extra request per site that needed it. Ignored unless the option above is on.

## Actor input object example

```json
{
  "searchTerms": [
    "plumber",
    "hvac contractor"
  ],
  "locations": [
    "Chicago, IL",
    "Austin, TX"
  ],
  "sources": [
    "bbb",
    "duckduckgo"
  ],
  "maxResultsPerSearch": 100,
  "maxResults": 0,
  "onlyWithContact": false,
  "findEmails": false,
  "followContactPage": true
}
```

# Actor output Schema

## `contacts` (type: `string`):

No description

## `outreach` (type: `string`):

No description

## `hours` (type: `string`):

No description

## `map` (type: `string`):

No description

## `all` (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 = {
    "searchTerms": [
        "plumber",
        "hvac contractor"
    ],
    "locations": [
        "Chicago, IL",
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lightmoon/local-business-phone-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 = {
    "searchTerms": [
        "plumber",
        "hvac contractor",
    ],
    "locations": [
        "Chicago, IL",
        "Austin, TX",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("lightmoon/local-business-phone-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 '{
  "searchTerms": [
    "plumber",
    "hvac contractor"
  ],
  "locations": [
    "Chicago, IL",
    "Austin, TX"
  ]
}' |
apify call lightmoon/local-business-phone-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lightmoon/local-business-phone-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/YhuvMC3BMdNQnBOXF/builds/ZWIwqI1ZgUcRsfdxy/openapi.json
