# Yelp Scraper: Business Search & Category Coverage — $4/1k (`chimerical_quicklime/yelp-business-search-scraper`) Actor

Yelp Scraper for business search by city and service: ratings, phone, website, hours, claimed status, plus a coverage mode counting businesses per city and service to find market gaps. Yelp caps counts at 240. No login or cookies. MCP-ready for AI agents. $4 per 1,000 records.

- **URL**: https://apify.com/chimerical_quicklime/yelp-business-search-scraper.md
- **Developed by:** [Khrystyna Skotte](https://apify.com/chimerical_quicklime) (community)
- **Categories:** Lead generation, Automation, Developer 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 result rows

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?

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

## Yelp Scraper: Business Search, Details & Category Coverage

Search Yelp by city and service and get structured business data back: name, rating, review count, price level, categories, neighborhood, phone, website, address, coordinates, opening hours, claimed status and more. A second mode, **coverage**, returns one cheap row per city x service with the number of businesses Yelp lists and the top three names, so you can see in minutes where a market is crowded and where it is thin.

No Yelp account, no cookies, no API key. You pay per row, and the cost of getting past Yelp's bot wall is included in that price (see "Proxy and cost" below).

### What you get

The actor has two modes.

| Mode | One row is | Use it for |
|---|---|---|
| `coverage` | one city x service pair | Supply checks, market-gap research, territory planning |
| `listings` | one business | Lead lists, directory data, competitor mapping |

The mode is chosen for you: an empty input (`{}`) runs a small `coverage` sample, and any input you give runs `listings` unless you set `mode` yourself.

#### Coverage row fields

| Field | Example | Meaning |
|---|---|---|
| `recordType` | `coverage` | Row type |
| `marketplace` | `yelp` | Source |
| `city`, `service` | `San Diego, CA`, `dog grooming` | The pair you searched |
| `categoryExists` | `true` | `true` when Yelp returns results, `false` when it says "No results" (a visible gap) |
| `proCount` | `240` | Number of businesses Yelp reports for the search |
| `proCountCapped` | `true` | `true` when `proCount` hit Yelp's 240 ceiling, so the real number is higher |
| `resultCount` | `10` | Organic businesses on the first page |
| `adCount` | `10` | Sponsored results on the first page (not counted in `proCount`) |
| `topPros` | `["Pet Beloved", ...]` | Names of the first three organic businesses |
| `topProsDetail` | `[{name, rating, reviewCount, bizUrl}]` | The same three with rating, reviews and URL |
| `pageTitle`, `pageUrl` | `Top 10 Best dog grooming Near San Diego...` | The Yelp page counted |
| `matchedSlug` | `null` | Always null on Yelp; kept so the row shape matches the sibling Thumbtack, Angi and Bark coverage actors |
| `scrapedAt` | ISO timestamp | When it was read |
| `error` | text | Only on failure. Error rows are not charged |

#### Business row fields

Every business row has these from the search page:

`recordType`, `marketplace`, `searchCity`, `searchService`, `position` (organic rank), `isAd`, `bizId`, `alias`, `name`, `rating`, `reviewCount`, `priceRange` (`$` to `$$$$`), `categories[]`, `phone` (see note), `address`, `city`, `neighborhoods[]`, `serviceArea`, `yearsInBusiness`, `highlights[]`, `verifiedLicenses`, `yelpGuaranteed`, `snippet` (the review excerpt shown on the card), `photoUrl`, `bizUrl`, `totalResults`, `pageUrl`, `scrapedAt`.

With **business details** (`includeDetails: true`, or any URL in `bizUrls`) each row also gets:

`website`, `menuUrl`, `street`, `state`, `postalCode`, `country`, `latitude`, `longitude`, `hours[]` (per day), `isClaimed`, `isClosed`, `isYelpAdvertiser`, `yearEstablished`, `history`, `specialties`, `summary`, `photoCount`, `amenities[]`, `healthScore`, `healthInspectedAt`, and `detailsError` if that one page failed.

Detail values that are empty never overwrite what the search card already had.

**Phone numbers:** Yelp does not put the phone number in organic search cards. Organic rows get a phone only when you turn on `includeDetails` (or pass `bizUrls`). Sponsored rows carry their phone on the card.

### Real output

#### Coverage (cloud run, `{}` input, San Diego)

```json
{
  "recordType": "coverage",
  "marketplace": "yelp",
  "city": "San Diego, CA",
  "service": "dog grooming",
  "categoryExists": true,
  "proCount": 240,
  "proCountCapped": true,
  "resultCount": 10,
  "adCount": 10,
  "topPros": ["Pet Beloved", "Pawadise Grooming", "Wags & Ruffs"],
  "topProsDetail": [
    {"name": "Pet Beloved", "rating": 4.9, "reviewCount": 243, "bizUrl": "https://www.yelp.com/biz/pet-beloved-san-diego-2"},
    {"name": "Pawadise Grooming", "rating": 4.9, "reviewCount": 49, "bizUrl": "https://www.yelp.com/biz/pawadise-grooming-san-diego"},
    {"name": "Wags & Ruffs", "rating": 4.8, "reviewCount": 164, "bizUrl": "https://www.yelp.com/biz/wags-and-ruffs-san-diego"}
  ],
  "matchedSlug": null,
  "pageTitle": "Top 10 Best dog grooming Near San Diego, California (Updated 2026)",
  "pageUrl": "https://www.yelp.com/search?find_desc=dog+grooming&find_loc=San+Diego%2C+CA",
  "scrapedAt": "2026-10-22T02:10:18.402Z"
}
```

#### Business with details (cloud run, Austin plumbers, `includeDetails: true`)

```json
{
  "recordType": "business",
  "marketplace": "yelp",
  "searchCity": "Austin, TX",
  "searchService": "plumbers",
  "position": 1,
  "isAd": false,
  "bizId": "TVHztfQAJUIaJDgdJuke-g",
  "alias": "blue-dragon-plumbing-austin-2",
  "name": "Blue Dragon Plumbing",
  "rating": 4.9,
  "reviewCount": 532,
  "priceRange": null,
  "categories": ["Plumbing"],
  "phone": "(512) 947-2491",
  "address": "Austin, TX 78727",
  "city": "Austin",
  "neighborhoods": ["Milwood"],
  "verifiedLicenses": 0,
  "yelpGuaranteed": false,
  "snippet": "They are fast, reliable and trustworthy. If you're looking for a plumbing service, look no further.",
  "bizUrl": "https://www.yelp.com/biz/blue-dragon-plumbing-austin-2",
  "website": "http://bluedragonplumbing.com",
  "state": "TX",
  "postalCode": "78727",
  "country": "US",
  "hours": [
    {"day": "Mon", "hours": ["8:00 AM - 5:00 PM"]},
    {"day": "Sat", "hours": ["9:00 AM - 5:00 PM"]},
    {"day": "Sun", "hours": ["9:00 AM - 6:00 PM"]}
  ],
  "isClaimed": true,
  "isClosed": false,
  "yearEstablished": "2011",
  "photoCount": 33,
  "totalResults": 240,
  "scrapedAt": "2026-10-22T02:11:05.464Z"
}
```

(Hours shortened to three days here; the real row has all seven. `history` and `specialties` free text are also returned.)

#### Business page by URL (cloud run)

A business URL returns the full record, for example Extraordinary Desserts in San Diego: phone `(619) 294-2132`, website, `menuUrl`, `latitude` 32.73536, `longitude` -117.161405, `isClaimed` true, `photoCount` 8197, `healthScore` "99 out of 100" with `healthInspectedAt`.

### Use cases

1. **Find category gaps per city.** Run `coverage` for 50 cities x 20 services. Rows with `categoryExists: false`, or a low `proCount` that is not capped, are underserved markets. Join with the same pairs from the Thumbtack, Angi or Bark coverage actors to see where Yelp has supply and the marketplaces have none.
2. **Local lead lists.** Run `listings` with `includeDetails` for a trade in a metro. You get name, phone, website, address, claimed status and years in business per row, ready for a CRM import.
3. **Franchise and territory planning.** Compare `proCount`, ratings and review counts across zip codes (Yelp accepts a zip in `cities`).
4. **Competitor mapping.** Pull every business in your category within a city, sorted by Yelp's own ranking (`position`), with price level and review volume.
5. **Unclaimed-business outreach.** Filter detail rows by `isClaimed: false` or `isYelpAdvertiser: false` to find businesses that are not managing their Yelp page.
6. **Closure and health checks.** `isClosed` and `healthScore` on detail rows help clean directories and screen restaurant lists.
7. **Scheduled market tracking.** Run `coverage` weekly on a schedule and compare `proCount` over time (while it is below 240).

### Input

| Input | Type | Default | What it does |
|---|---|---|---|
| `mode` | `coverage` / `listings` | automatic | `coverage` = one row per city x service. `listings` = one row per business. Empty: `coverage` for `{}`, `listings` when you give inputs |
| `cities` | string list | none | Locations such as `San Diego, CA`, `Austin, TX`, `10001`. Every service is searched in every city |
| `services` | string list | none | Search terms such as `dog grooming`, `plumbers`, `pizza` |
| `listUrl` | text | empty | CSV or Google Sheet shared as "anyone with the link". Each row is `city,service` or a Yelp URL. A header row is skipped |
| `searchUrls` | string list | none | Yelp search URLs with your own filters, kept as given |
| `bizUrls` | string list | none | Yelp business URLs (`https://www.yelp.com/biz/...`), one full-detail row each |
| `maxItems` | integer | `10` | Businesses per search in `listings` mode, 1 to 240 |
| `includeDetails` | boolean | `false` | `listings`: also open each business page (adds phone, website, hours and more). Extra charge per business, see pricing |
| `includeAds` | boolean | `false` | `listings`: also return sponsored rows, flagged `isAd: true` |
| `concurrency` | integer | `4` | Searches run in parallel, 1 to 10 |
| `proxyConfiguration` | proxy | empty | Leave empty. The actor uses Apify's Unblocker proxy automatically |

Older input names still work: `searchTerms`, `searchTerm`, `query` (services), `locations`, `location` (cities), `searchLimit` (`maxItems`), `directUrls` and `startUrls` (search and business URLs are told apart automatically).

#### Example: coverage for a list of cities and services

```json
{
  "mode": "coverage",
  "cities": ["San Diego, CA", "Austin, TX", "Ely, NV"],
  "services": ["dog grooming", "house cleaning", "chimney sweep"]
}
```

Nine pairs, nine rows.

#### Example: plumbers in Austin with phone and website

```json
{
  "mode": "listings",
  "cities": ["Austin, TX"],
  "services": ["plumbers"],
  "maxItems": 50,
  "includeDetails": true
}
```

#### Example: one business, by URL

```json
{ "bizUrls": ["https://www.yelp.com/biz/extraordinary-desserts-san-diego"] }
```

#### Example: your own Yelp filters

```json
{ "searchUrls": ["https://www.yelp.com/search?find_desc=Plumbers&find_loc=Austin%2C+TX"], "maxItems": 30 }
```

#### Example: bulk list from a Google Sheet

Share the sheet as "anyone with the link", put `city,service` in two columns, and pass:

```json
{ "mode": "coverage", "listUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit" }
```

### Pricing

Pay per event. You are charged only for rows that are delivered.

| Event | Price | When |
|---|---|---|
| Actor start | $0.001 | once per run |
| Row | $0.004 ($4 per 1,000) | each coverage row and each business row |
| Business details | $0.003 ($3 per 1,000) | each business whose page is opened (`includeDetails`, or each `bizUrls` entry) |

What that means in practice:

- **1,000 coverage rows** (for example 50 cities x 20 services): about $4.00 plus the $0.001 start.
- **1,000 businesses, search data only:** about $4.00. Remember each search returns at most 240, so 1,000 businesses means at least five searches.
- **1,000 businesses with phone, website and hours:** about $7.00 ($4 rows + $3 details).
- Coverage rows that fail are marked with `error` and are not charged. If a business page fails to open, the business row is still delivered (with `detailsError`) and only the details charge is skipped.

Platform compute is small: the actor needs only 512 MB because it does plain HTTP, no browser.

### Proxy and cost

Yelp runs a bot wall (DataDome). In testing, plain requests, datacenter proxies, residential proxies and a real headed browser were all blocked from the cloud. The only route that worked was Apify's **Unblocker** proxy, which the actor uses by default. Its charges are billed to the actor owner and are already part of the per-row price above, so you do not pay a separate proxy bill. Because the Unblocker is only available inside Apify runs, the actor cannot be run on your own machine; run it on Apify and call it through the API or a schedule.

Each search or business page takes roughly 10 to 30 seconds through the unblocker, so a large `listings` run with details takes minutes, not seconds. Raising `concurrency` speeds up multi-search jobs.

### Limits you should know about

Stated plainly so there are no surprises:

- **Yelp caps every search at 240 results.** Popular searches such as "dog grooming" in San Diego return exactly 240, and `proCountCapped: true` marks it. A capped count means "at least 240", not the true total. Counts below 240 are real: in our tests Ely, NV returned 2 for dog grooming and 0 for chimney sweep. To go past 240, split the area (zip codes or neighborhoods in `cities`) and add the results.
- **Review texts are not included.** The row has `rating`, `reviewCount` and one review excerpt (`snippet`), not the reviews themselves. Use a dedicated Yelp reviews actor for that.
- **Sponsored results are excluded by default.** They are not counted in `proCount` or `resultCount`. `adCount` tells you how many appeared on page one. With `includeAds: true` they are returned in `listings` and flagged `isAd: true`.
- **Photos:** you get the lead photo URL and the photo count, not the photo gallery with captions.
- **Hours:** `hours` holds this week's hours as Yelp shows them, with special hours merged in. Alternate business names are not output.
- **Coordinates and some fields** exist only on detail rows, and service-area businesses with no map have no latitude or longitude.
- Fields Yelp does not show for a given business come back `null` or empty.
- If Yelp changes its page structure, the search parser fails loudly and the affected pair is marked `error` in the run's `OUTPUT` record rather than returning wrong data.

### How it compares to the other Yelp actors

Measured from the Store pages and actor records on 2026-10-21:

- **tri_angle/yelp-scraper** (rated 2.7 on 10 ratings) takes search terms, locations and direct URLs. Its README also says "up to 240 results on average", the same Yelp cap. It prices per business in tiers from $0.005 on the free plan down to $0.0008 on the highest plan, plus a charge for photo pages. It has no coverage mode and no bulk list input. Our flat $0.004 row is below its free-plan price; its higher-tier prices are lower than ours, and its rows include photos with captions that ours do not.
- **tri_angle/yelp-review-scraper** and **web_wanderer/yelp-reviews-scraper** return review text only. They are a different product from this one and are the right tool if reviews are what you need. This actor is for the business records and market counts.

What this actor adds: a `coverage` mode with one row even for empty results, `proCountCapped` and `adCount` so you know how far to trust a count, a CSV or Google Sheet bulk list, sponsored rows separated from organic ones, and business details (phone, website, claimed status, coordinates, hours) in a single run.

### FAQ

**Why is my phone column empty?** Organic search cards on Yelp do not include the phone. Turn on `includeDetails`.

**Why does every big city show 240?** That is Yelp's cap on results per search, flagged by `proCountCapped`. Use smaller areas to count further.

**Does `categoryExists: false` mean nobody offers the service?** It means Yelp returned "No results" for that search at that location. Always check the `pageUrl`; Yelp may match the term differently from how locals name the service, so try a synonym.

**Can I search a zip code?** Yes, put it in `cities`.

**Is anything stored between runs?** No. Every run reads Yelp fresh. To track change over time, schedule the actor and compare datasets.

**What happens if some pages fail?** Rows that succeeded are kept. Failed targets are listed in the run's `OUTPUT` record with a status, and failed coverage rows carry an `error` and are not charged.

### Integrations

- **API:** start a run with `POST https://api.apify.com/v2/acts/chimerical_quicklime~yelp-business-search-scraper/runs?token=YOUR_TOKEN` and the input JSON as the body. Fetch rows from `/v2/datasets/DATASET_ID/items?format=csv` (or `json`, `xlsx`).
- **Python:**

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_TOKEN")
run = client.actor("chimerical_quicklime/yelp-business-search-scraper").call(run_input={
    "mode": "coverage",
    "cities": ["San Diego, CA"],
    "services": ["dog grooming", "house cleaning"],
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["city"], row["service"], row["proCount"], row["proCountCapped"])
```

- **JavaScript:**

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('chimerical_quicklime/yelp-business-search-scraper').call({
    mode: 'listings', cities: ['Austin, TX'], services: ['plumbers'], maxItems: 20, includeDetails: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

- **Schedules and webhooks:** run weekly from the Apify console and send a webhook to your own endpoint when a run finishes.
- **Make, Zapier, n8n:** use the Apify module or HTTP request with the run API above; push the dataset to Google Sheets, Airtable or a CRM.
- **MCP / AI agents:** the actor can be called as a tool through Apify's MCP server, so an agent can ask "how many dog groomers does Yelp list in Austin?" and get a coverage row back.

### Legal

The actor reads publicly visible Yelp pages. You are responsible for using the data in line with Yelp's terms and the privacy and marketing laws that apply to you, especially when contacting businesses.

# Actor input Schema

## `mode` (type: `string`):

coverage = one row per city x service with Yelp's result count and the top 3 businesses (cheap supply check). listings = one row per business from each search. Leave empty: listings when you give inputs, a coverage sample when you give none.

## `cities` (type: `array`):

Where to search, e.g. "San Diego, CA", "Austin, TX", "10001". Every service is searched in every city.

## `services` (type: `array`):

What to search for, e.g. "dog grooming", "plumbers", "pizza".

## `listUrl` (type: `string`):

A CSV or a Google Sheet shared as "anyone with the link". Each row is either city,service (two columns) or a Yelp search/business URL. A header row is skipped.

## `searchUrls` (type: `array`):

Yelp search pages with your own filters, e.g. https://www.yelp.com/search?find_desc=Plumbers\&find_loc=Austin%2C+TX

## `bizUrls` (type: `array`):

Business pages (https://www.yelp.com/biz/...). Each returns one full row: phone, website, address, hours, claimed status, coordinates.

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

Listings mode: businesses to return per search (Yelp shows at most 240 per search).

## `includeDetails` (type: `boolean`):

Listings mode: also open each business page for phone, website, full address, coordinates, hours, claimed status, photo count and health score. One extra page per business.

## `includeAds` (type: `boolean`):

Listings mode: also return Yelp's sponsored rows (flagged isAd). Coverage counts never include them.

## `concurrency` (type: `integer`):

How many searches run at once.

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

Leave empty: the actor uses Apify's UNBLOCKER proxy, which is what gets past Yelp's bot wall.

## Actor input object example

```json
{
  "maxItems": 10,
  "includeDetails": false,
  "includeAds": false,
  "concurrency": 4
}
```

# Actor output Schema

## `records` (type: `string`):

Dataset of coverage and business rows (JSON).

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("chimerical_quicklime/yelp-business-search-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("chimerical_quicklime/yelp-business-search-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 '{}' |
apify call chimerical_quicklime/yelp-business-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,chimerical_quicklime/yelp-business-search-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/T2pDeCt4l9gXdf8cD/builds/GfTKyiooLaSuEaGjr/openapi.json
