# BBB Scraper: Leads, Reviews and Complaints by City (`solalab_digital/bbb-leads-complaints-scraper`) Actor

Scrapes Better Business Bureau (bbb.org) search results for the USA and Canada past the 225-result cap, with optional customer reviews and complaints, complaint themes, a 0-100 lead score, change monitoring and an HTML report.

- **URL**: https://apify.com/solalab_digital/bbb-leads-complaints-scraper.md
- **Developed by:** [Sankov Vadim](https://apify.com/solalab_digital) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 results

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

## BBB Scraper: Better Business Bureau Leads, Reviews and Complaints

Search bbb.org by category and city, get clean business records with phone, rating, accreditation and a lead score, and optionally pull the full review and complaint texts.

**It runs on plain HTTP requests without a browser, works around BBB's 225-result cap, and can flag what changed since your last run.**

### 🚀 Quick start

1. Open the Actor and keep the prefilled input: search term `plumber`, location `Austin, TX`, country `United States`.
2. Click **Start**. The prefill runs without the 225-limit split and without reviews or complaints, so it finishes fast.
3. Open the **Overview** or **Leads** view of the dataset, or open `REPORT` in the key-value store for the HTML report. Export to CSV, Excel or JSON.

To get a whole market instead of the first 225 results, turn on **Get past the 225-result limit**. To read what customers say, turn on **Include customer reviews** and **Include complaints**.

### 💡 Use cases

- **Lead generation for B2B sellers and agencies.** Build lists of local contractors and service companies with phone, address, category, BBB rating and a 0-100 lead score to sort by.
- **Contractor and vendor checks.** Look up a specific company by exact name and see its rating, accreditation, complaint history and how often it answers complaints.
- **Reputation and competitor analysis.** Pull complaint texts and company replies for a whole category, grouped by theme (refund/billing, quality, delivery/delay, customer service, fraud/misleading).
- **Monitoring.** Re-run the same search on a schedule and get only the businesses that are new or changed (rating, accreditation, complaints).

### ✨ Why this Actor

| Feature | What you get |
|---|---|
| HTTP only | Reads three open JSON endpoints on bbb.org, so it needs neither a browser nor a login. |
| Search by category or name | Category/keyword plus city, state or ZIP. `searchByName` for exact company lookups. US and Canada. |
| 225-result workaround | Splits a capped search by sub-category, then by nearby ZIP codes sorted by distance, then by sort order. Duplicates removed by `businessId`. |
| Coverage report | `COVERAGE` in the key-value store shows, for every query, how many results BBB reports and how many you collected. |
| Reviews and complaints | Texts, stars, dates, status and company replies. Counts by status and type cover all complaints on file, not only the saved ones. |
| Complaint themes | Each complaint is tagged: refund/billing, quality, delivery/delay, customer service, fraud/misleading, other. |
| Lead score | One 0-100 number built from rating, accreditation, reviews, complaint handling and phone. |
| Monitor mode | Marks each business NEW, CHANGED or UNCHANGED against your previous run. |
| HTML report | Self-contained page with sorting, filters and CSV download. |
| Reliable runs | Retries with backoff, spending limit respected, collected data saved on timeout or migration. |

In one smoke run for `plumber` in Austin, TX, BBB reported 2544 results for the search and showed 225. With splitting on, the Actor collected 1048 unique businesses.

### ⚙️ Input

| Field | Type | Default | Description |
|---|---|---|---|
| `searchTerms` | list | `plumber` (prefill) | Categories (`roofing contractors`) or business names. Each term runs in every location. |
| `locations` | list | `Austin, TX` (prefill) | `City, ST`, a ZIP code, or a Canadian city with province (`Toronto, ON`). |
| `country` | `USA` / `CAN` | `USA` | BBB covers the United States and Canada. |
| `startUrls` | list | empty | Optional bbb.org search URLs or category URLs. |
| `searchByName` | boolean | `false` | BBB matches terms loosely. Turn on to look up a company by its name. |
| `sort` | select | `Relevance` | `Relevance`, `Distance`, `Rating`, `AToZ`, `ZToA`. |
| `maxItems` | integer | 1000 (prefill 20) | Total unique businesses to save across all searches. |
| `maxItemsPerQuery` | integer | 0 | Limit per term + location pair. 0 means no limit. |
| `autoSplit` | boolean | `true` (prefill `false`) | Get past BBB's 225-result cap. |
| `maxSplitRequests` | integer | 300 | Cap on extra search pages used for splitting. Each page holds 15 results. |
| `accreditedOnly` | boolean | `false` | Keep only BBB Accredited Businesses. |
| `minRating` | select | `any` | Drop businesses rated below this grade (`A+` to `F`). Not-rated businesses are dropped when a grade is set. |
| `strictMatch` | boolean | `false` | Keep only businesses whose name or categories contain every word of the term. |
| `includeReviews` | boolean | `false` | Fetch review texts, stars and business replies. |
| `maxReviewsPerBusiness` | integer | 30 | Newest reviews first. |
| `includeComplaints` | boolean | `false` | Fetch complaint texts, type, status and business responses. |
| `maxComplaintsPerBusiness` | integer | 30 | Most recently closed first. Counts by status and type always cover all complaints. |
| `monitorMode` | boolean | `false` | Compare with the previous run under the same monitor name. |
| `monitorKey` | string | `default` | Runs with the same name share one snapshot. |
| `onlyChanges` | boolean | `false` | With monitoring on, unchanged businesses are not saved and not charged. |
| `buildReport` | boolean | `true` | Save the HTML report as `REPORT`. |
| `maxConcurrency` | integer | 4 | Parallel requests, 1 to 10. Higher is faster but may hit rate limits. |
| `proxyConfiguration` | object | no proxy | Usually not needed. Turn on if the log shows blocking warnings. |

Example input:

```json
{
  "searchTerms": ["plumber"],
  "locations": ["Austin, TX"],
  "country": "USA",
  "maxItems": 500,
  "autoSplit": true,
  "includeComplaints": true
}
```

### 📦 Output

One row per business. The dataset has three views: **Overview**, **Leads** (contact and location fields) and **Complaints** (complaint stats per business).

| Field | Description |
|---|---|
| `businessId`, `bbbId`, `searchId` | BBB identifiers. `businessId` is used for deduplication. |
| `businessName`, `primaryCategory`, `categories` | Name and categories. |
| `address`, `city`, `state`, `postalCode`, `country` | Location. |
| `phone`, `phones` | First phone and all phones. |
| `latitude`, `longitude` | Coordinates. |
| `rating`, `ratingScore` | BBB letter grade and numeric score. |
| `isAccredited`, `bbbOffice` | Accreditation flag and local BBB office. |
| `profileUrl`, `requestAQuoteUrl`, `leaveReviewUrl`, `logoUrl` | Links. |
| `outOfBusinessStatus`, `isCharity`, `charitySeal` | Status flags. |
| `serviceAreasSummary`, `hasServiceArea`, `serviceAreaDescription` | Service area. |
| `leadScore`, `leadScoreBreakdown` | 0-100 score and its components. |
| `searchTerm`, `searchLocation`, `splitPath`, `scrapedAt` | Where and when the row was found. |
| `reviewsCount`, `reviewsAvgStars`, `reviewsBusinessResponseRate` | Review stats (with reviews on). |
| `reviews[]` | `id`, `stars`, `author`, `date`, `text`, `businessResponse`, `businessResponseDate`, `businessRebuttal`, `customerResponse`. |
| `complaintsCount`, `complaintsHidden` | Complaints on file and hidden ones (with complaints on). |
| `complaints[]` | `id`, `type`, `status`, `date`, `text`, `businessResponse`, `consumerResponse`, `category`. |
| `complaintsByStatus`, `complaintsByType` | Counts across all complaints on file. |
| `complaintsResponseShare`, `complaintsResolvedShare` | Share of complaints answered and resolved. |
| `complaintCategories`, `topComplaintCategory` | Theme counts and the most common theme. |
| `monitorStatus`, `changedFields`, `newComplaints` | Monitor results (with monitoring on). |
| `yearsInBusiness` | Always `null` in v1. See Limits. |
| `detailsError` | Set when reviews or complaints could not be fetched. The business row is still saved. |

Real row from a run on the Apify platform (`plumber` in Austin, TX, reviews and complaints on; texts and lists shortened):

```json
{
  "businessId": "90006300",
  "bbbId": "0825",
  "businessName": "EC Design Builds Inc",
  "address": null,
  "city": "Austin",
  "state": "TX",
  "postalCode": "78731",
  "country": "USA",
  "phone": "(512) 363-3576",
  "rating": "A+",
  "ratingScore": 100,
  "isAccredited": true,
  "primaryCategory": "General Contractor",
  "primaryCategoryId": "10035-000",
  "categories": [
    {
      "id": "10035-000",
      "name": "General Contractor"
    },
    {
      "id": "10126-000",
      "name": "Roofing Contractors"
    },
    {
      "id": "10032-000",
      "name": "Concrete Contractors"
    }
  ],
  "profileUrl": "https://www.bbb.org/us/tx/austin/profile/general-contractor/ec-design-builds-inc-0825-90006300",
  "isCharity": false,
  "outOfBusinessStatus": null,
  "hasServiceArea": true,
  "serviceAreasSummary": [
    "Bastrop County, TX",
    "Blanco County, TX",
    "Guadalupe County, TX",
    "Travis County, TX",
    "Williamson County, TX"
  ],
  "yearsInBusiness": null,
  "searchTerm": "plumber",
  "searchLocation": "Austin, TX",
  "splitPath": null,
  "scrapedAt": "2026-10-03T07:23:57+00:00",
  "reviewsCount": 3,
  "reviewsFetched": 3,
  "reviewsAvgStars": 5,
  "reviewsBusinessResponseRate": 0,
  "reviewIssueCategories": {},
  "reviews": [
    {
      "id": "0825_90006300_586722",
      "stars": 5,
      "author": "Phil D",
      "date": "2026-07-30",
      "text": "******* and ***** were absolutely great from the first phone call through project completion. They helped us navigate both our insurance claim and supplement...",
      "businessResponse": null,
      "businessResponseDate": null,
      "businessRebuttal": null,
      "customerResponse": null,
      "issueCategory": null
    }
  ],
  "complaintsCount": 1,
  "complaintsHidden": 0,
  "complaintsFetched": 1,
  "complaintsByStatus": {
    "answered": 1
  },
  "complaintsByType": {
    "serviceorrepair": 1
  },
  "complaintsResolvedShare": 0,
  "complaintsResponseShare": 1,
  "complaintCategories": {
    "fraud_misleading": 0,
    "refund_billing": 0,
    "delivery_delay": 0,
    "quality": 0,
    "customer_service": 0,
    "other": 1
  },
  "topComplaintCategory": "other",
  "lastComplaintId": "0825_90006300_24526559",
  "complaints": [
    {
      "id": "0825_90006300_24526559",
      "type": "Service or Repair Issues",
      "status": "Answered",
      "date": "2026-02-17",
      "text": "We expect bathroom work to be completed as per contract",
      "businessResponse": "EC Design Builds Inc. respectfully disputes the customers version of events and the assertion that contracted work was not completed. This project involved m...",
      "consumerResponse": "Complaint: 24526559 I am rejecting this response because: The Contractor is not complete with work. I have attached a pdf with additional information as rebu...",
      "responseCount": 4,
      "category": "other"
    }
  ],
  "detailsError": null,
  "leadScore": 93,
  "leadScoreBreakdown": {
    "rating": 100,
    "accreditation": 100,
    "reviews": 68.8,
    "complaints": 95,
    "contact": 100
  }
}
```

With complaints on, each business also gets `complaintsCount`, `complaintsByStatus`, `complaintsResponseShare`, `complaintCategories`, `topComplaintCategory` and a `complaints[]` list in the shape listed above.

### 📊 Report and coverage

When `buildReport` is on, the key-value store gets two records:

- **`REPORT`**: one HTML file with no external scripts. It shows totals, accredited count, average lead score, coverage, rating distribution, top categories and cities, top complaint themes, and a table you can sort by clicking headers and filter by rating, accreditation, city and minimum lead score. A button downloads the table as CSV.
- **`COVERAGE`**: JSON with, for each source query, the total BBB reports, how many you collected, how many splits were used and the shortfall. The log warns when more than 20% is missing.

### 🎯 Lead score

`leadScore` is a weighted average on a 0-100 scale. A component that is not available drops out, and its weight is not counted against the business.

| Component | Weight | Score |
|---|---|---|
| BBB rating | 0.35 | `ratingScore` if present, otherwise A+ = 100 down to F = 0. Not rated: left out. |
| Accreditation | 0.20 | 100 if accredited, otherwise 0. |
| Reviews | 0.20 | Average stars scaled to 0-100, pulled toward 50 when there are few reviews. Only with reviews on. |
| Complaint handling | 0.15 | 100 if no complaints, otherwise the answered share minus a penalty that grows with the number of complaints. Only with complaints on. |
| Phone | 0.10 | 100 if a phone number exists, otherwise 0. |

A business marked out of business gets a score of 0. `leadScoreBreakdown` shows each component for every row.

### 🔔 Monitoring

Turn on `monitorMode` and give the watch list a `monitorKey`. Each run compares against the saved snapshot for that name:

- `NEW`: not seen in earlier runs.
- `CHANGED`: something differs. `changedFields` lists what (rating, accreditation, complaints and so on), and `newComplaints` shows the increase.
- `UNCHANGED`: nothing moved.

Set `onlyChanges` to save only new and changed businesses. Unchanged ones are not saved and not charged. Schedule the Actor in Apify Console for daily or weekly checks. The snapshot is merged, not replaced, so a business missing from a later partial search is not marked as gone.

### 💳 Pricing

Pay per result. See the Pricing tab for current rates.

### ⚠️ Limits

- **You will not get owner names, emails, websites or years in business.** bbb.org profile pages sit behind Cloudflare and the Actor reads only the public JSON endpoints. `yearsInBusiness` is always `null`.
- **Big markets need a split budget.** Coverage depends on `maxSplitRequests` and on how BBB filters the query. Check `COVERAGE` to see what was missed, and raise the cap if needed.
- **Complaint themes are a heuristic.** Keyword rules, not a language model. Expect misclassified edge cases. BBB's own complaint `type` is kept next to it.
- **Search is loose.** BBB matches terms broadly. Use `searchByName` or `strictMatch` when you need precision.
- **The Actor covers the USA and Canada only.**
- **Blocking is possible.** If the log shows Cloudflare warnings, enable the proxy.

### ❓ FAQ

**Do I need a proxy?**
Usually not. Search, reviews and complaints loaded without one in our tests. Enable it if the log warns about blocking.

**Why does BBB show only 225 results?**
BBB caps every search at 225 results. With `autoSplit` on, the Actor repeats the search by sub-category, by nearby ZIP code and by sort order, then removes duplicates.

**How do I find one specific company?**
Put the company name in `searchTerms`, set a location and turn on `searchByName`.

**Can I scrape Canada?**
Yes. Set `country` to `CAN` and use locations like `Toronto, ON`.

**Are all complaints included in the counts?**
Yes. Status and type counts cover every complaint on file. The `complaints[]` list is limited by `maxComplaintsPerBusiness`.

**What happens if the run times out or hits my spending limit?**
The Actor stops sending new requests, saves what it has and writes the report.

### 📝 Changelog

- **1.0 (2026-10):** First release. Search, 225-limit split, reviews, complaints, complaint themes, lead score, monitor mode, HTML report.

### 💬 Support

Found a bug or need a field? Open an issue on the Actor's Issues tab in Apify Console. Include the run link and your input.

This Actor collects publicly available business information from bbb.org. You are responsible for using the data in line with the law and the site's terms in your jurisdiction.

# Actor input Schema

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

What to search for: a category ("plumber", "roofing contractors") or a business name. Every term is searched in every location.

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

"City, ST" ("Austin, TX"), a ZIP code, or a Canadian city with its province ("Toronto, ON").

## `country` (type: `string`):

BBB covers the USA and Canada.

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

Optional. bbb.org search URLs (bbb.org/search?find_text=...\&find_loc=...) or category URLs (bbb.org/us/tx/austin/category/plumber).

## `searchByName` (type: `boolean`):

BBB matches terms loosely ("Comcast" also returns "East Coast ..."). Turn on to look up a specific company by its name.

## `sort` (type: `string`):

Order in which BBB returns results.

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

Total number of unique businesses to save across all searches. Keeps run size and cost under control.

## `maxItemsPerQuery` (type: `integer`):

Limit for each term + location pair. 0 = no limit.

## `autoSplit` (type: `boolean`):

BBB shows at most 225 results per search. When a search has more, it is split into sub-categories, nearby ZIP codes and other sort orders, and duplicates are removed.

## `maxSplitRequests` (type: `integer`):

Safety cap on extra search pages used to get past the 225 limit (each page = 15 results).

## `accreditedOnly` (type: `boolean`):

Keep only BBB Accredited Businesses.

## `minRating` (type: `string`):

Drop businesses rated below this grade. Not-rated businesses are dropped when a grade is set.

## `strictMatch` (type: `boolean`):

Keep only businesses whose name or categories contain every word of the search term.

## `includeReviews` (type: `boolean`):

Fetch review texts, star ratings and business replies for every business.

## `maxReviewsPerBusiness` (type: `integer`):

Newest reviews first.

## `includeComplaints` (type: `boolean`):

Fetch complaint texts, type, status and business responses, plus counts by status and type for all complaints on file.

## `maxComplaintsPerBusiness` (type: `integer`):

Most recently closed first. Status and type counts always cover all complaints.

## `monitorMode` (type: `boolean`):

Compare with the previous run under the same monitor name: marks each business NEW, CHANGED or UNCHANGED and lists changed fields (rating, accreditation, complaints...).

## `monitorKey` (type: `string`):

Runs with the same name share one snapshot. Use different names for different watch lists.

## `onlyChanges` (type: `boolean`):

With monitoring on, unchanged businesses are not saved (and not charged).

## `buildReport` (type: `boolean`):

Save a self-contained HTML report (REPORT) to the key-value store.

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

Higher is faster but more likely to be rate-limited by BBB.

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

Usually not needed: BBB's search, review and complaint data loads without a proxy. Turn on if you see blocking warnings in the log.

## Actor input object example

```json
{
  "searchTerms": [
    "plumber"
  ],
  "locations": [
    "Austin, TX"
  ],
  "country": "USA",
  "searchByName": false,
  "sort": "Relevance",
  "maxItems": 20,
  "maxItemsPerQuery": 0,
  "autoSplit": false,
  "maxSplitRequests": 300,
  "accreditedOnly": false,
  "minRating": "any",
  "strictMatch": false,
  "includeReviews": false,
  "maxReviewsPerBusiness": 30,
  "includeComplaints": false,
  "maxComplaintsPerBusiness": 30,
  "monitorMode": false,
  "monitorKey": "default",
  "onlyChanges": false,
  "buildReport": true,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `report` (type: `string`):

KPIs, rating and category breakdowns, complaint themes, coverage per query and a sortable, filterable table with CSV export.

## `businesses` (type: `string`):

One row per business.

## `coverage` (type: `string`):

For every search: BBB's total result count, how many businesses were collected and how many split queries it took.

# 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"
    ],
    "locations": [
        "Austin, TX"
    ],
    "maxItems": 20,
    "autoSplit": false,
    "includeReviews": false,
    "includeComplaints": false,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("solalab_digital/bbb-leads-complaints-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"],
    "locations": ["Austin, TX"],
    "maxItems": 20,
    "autoSplit": False,
    "includeReviews": False,
    "includeComplaints": False,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("solalab_digital/bbb-leads-complaints-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"
  ],
  "locations": [
    "Austin, TX"
  ],
  "maxItems": 20,
  "autoSplit": false,
  "includeReviews": false,
  "includeComplaints": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call solalab_digital/bbb-leads-complaints-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,solalab_digital/bbb-leads-complaints-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/ksaboyHRepHbzjAWm/builds/zUDa8Fun71Uyd9zCu/openapi.json
