# BBB Scraper: Ratings, Accreditation, Complaints, Reviews (`dottti/bbb-business-scraper`) Actor

Search bbb.org (US and Canada) by keyword and city, state or ZIP, or paste BBB profile URLs. Get BBB rating, accreditation date, phone, website, years in business, complaint and review counts. Optional review and complaint text. Status rows are free.

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

## Pricing

from $1.50 / 1,000 businesses

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: Ratings, Accreditation, Complaints, Reviews

Get Better Business Bureau (bbb.org) data for businesses in the United States and Canada. Search by category or keyword and a city, state or ZIP code. You can also paste BBB profile links. Each business comes back as one flat row with its BBB letter grade, accreditation status, categories, phone numbers, address, map position, and the number of public complaints and customer reviews.

You can also add the public customer reviews and complaints of each business. They come as separate rows at a lower price.

You need no BBB account and no API key.

### Who uses it

- **Lead generation teams** who build lists of local contractors, dentists, lawyers or shops with phone numbers and addresses.
- **Due diligence and vendor checks.** See a supplier's BBB grade, accreditation and complaint history before you sign.
- **Insurance, lending and risk teams** who score small businesses with BBB grades and complaint counts.
- **Agencies and reputation managers** who track BBB ratings and complaints for their clients on a schedule.
- **Researchers** who compare complaint types across a category or a city.

### What you can do

- Search many terms in many locations in one run. Each term runs in each location.
- Paste BBB profile URLs. Review and complaint sub-page links work too.
- Keep only accredited businesses, or only businesses with a minimum BBB grade (for example A- or better).
- Get the number of complaints and reviews for every business, and the average stars when a business has 15 reviews or fewer.
- Add customer reviews (stars, date, text, business reply) and complaints (type, status, date, text, business response).
- Canadian locations such as `Toronto, ON` or `M4S 2N4` switch the search to Canada on their own.

### Input examples

Roofers in two Texas cities, accredited only:

```json
{
  "searchTerms": ["roofing"],
  "locations": ["Austin, TX", "Dallas, TX"],
  "accreditedOnly": true,
  "maxResultsPerSearch": 100
}
```

Plumbers and electricians in three ZIP codes, A- or better:

```json
{
  "searchTerms": ["plumber", "electrician"],
  "locations": ["10001", "60614", "94103"],
  "minRating": "A-",
  "maxResultsPerSearch": 50
}
```

Check a list of known businesses, with their newest complaints:

```json
{
  "profileUrls": [
    "https://www.bbb.org/us/tx/austin/profile/roofing-contractors/fiducia-roofing-restoration-0825-90019908",
    "https://www.bbb.org/ca/on/toronto/profile/dentist/central-dental-group-0107-1374844"
  ],
  "includeComplaints": true,
  "maxComplaintsPerBusiness": 30
}
```

With empty input the actor runs a small example: 10 roofing businesses in Austin, TX.

### Output

Every row has a `type` field: `business`, `review`, `complaint`, `search` or `status`.

A business row (real data from bbb.org, 3 November 2026):

```json
{
  "type": "business",
  "name": "Stevan Buren Roofing, Windows, and Flooring",
  "bbbRating": "A+",
  "accredited": true,
  "primaryCategory": "Roofing Contractors",
  "categories": ["Roofing Contractors", "Siding Contractors", "Windows", "Window Installation"],
  "phone": "(817) 558-6997",
  "additionalPhones": ["(210) 686-1425", "(214) 432-2593", "(512) 275-6159"],
  "street": "108 Wild Basin Rd Suite 250",
  "city": "Austin",
  "state": "TX",
  "postalCode": "78746",
  "country": "US",
  "latitude": 30.30679702758789,
  "longitude": -97.8266830444336,
  "complaintsTotal": 1,
  "reviewRating": 5,
  "reviewsCount": 2,
  "localBbb": "BBB Heart of Texas",
  "profileUrl": "https://www.bbb.org/us/tx/cleburne/profile/roofing-contractors/stevan-buren-roofing-windows-and-flooring-0825-235975973",
  "bbbId": "0825",
  "businessId": "235975973",
  "detailLevel": "search",
  "searchTerm": "roofing",
  "searchLocation": "Austin, TX"
}
```

A complaint row:

```json
{
  "type": "complaint",
  "businessName": "RR Plumbing Roto-Rooter",
  "complaintType": "Billing Issues",
  "complaintStatus": "Answered",
  "date": "2026-10-22",
  "text": "I used this company to deal with water damage in my basement due to burst pipe. ...",
  "businessResponse": "We have received the complaint and will review ...",
  "responses": [{ "from": "business", "date": "2026-10-23", "text": "..." }],
  "profileUrl": "https://www.bbb.org/us/oh/cincinnati/profile/plumber/roto-rooter-plumbing-water-cleanup-0292-1922"
}
```

A review row has `stars`, `date`, `text`, `businessResponse` and `businessResponseDate`. Reviewer names are left out on purpose. BBB already masks personal details inside complaint and review text.

Each search gets one free `search` row. It shows how many results BBB has, how many were delivered, how many your filters skipped, and a status: `ok`, `no_results`, `location_not_found` or `blocked`. A bad or missing profile URL gets a free `status` row (`invalid_input`, `not_found` or `blocked`).

`reviewRating` is filled only when it is exact: the business has 15 reviews or fewer, so every star is counted.

**Full profile fields (experimental).** Turn on `includeProfileDetails` to also try each profile page for website, accreditation date, years in business, entity type, social links and closed-complaint counts (last 3 years and 12 months). bbb.org often refuses these pages to cloud servers. When that happens the row keeps the search data, `detailLevel` stays `search`, and after 3 refusals the run stops trying. When a page is read, `detailLevel` is `full`.

The run summary (key-value store, `RUN_SUMMARY`) lists every input, profile pages read or refused, requests, kB used and the proxy tier used.

### How to schedule

Save your input as a task in Apify Console, then add a schedule (for example every Monday). Use the dataset API or an integration (Google Sheets, Slack, webhooks) to send the rows where you need them.

### Pricing

You pay per result row. Platform usage is included in the price.

- **Business row**: one charge per business delivered.
- **Review or complaint row**: one charge per row, at a lower price. Only charged when you turn these on.
- **Search and status rows are free.** You never pay for a blocked page, a bad URL, a location BBB does not know, or a business your filters skipped.

You can set a maximum cost per run in Apify Console. The actor stops cleanly when it reaches that limit.

### Limits

- BBB shows at most 225 results (15 pages) per search. For bigger areas, split by city or ZIP code, or use narrower terms.
- If BBB does not recognise a location, it shows a nationwide list. The actor then returns a free `location_not_found` row and delivers nothing for that search.
- The same business found by two searches is delivered once.
- Accreditation flags in Canadian search results can be incomplete.
- Website, years in business and accreditation date come only from profile pages, which bbb.org often refuses to cloud servers (see above).
- The actor reads public BBB pages only. It never logs in and never collects the names of reviewers or complainants.

# Actor input Schema

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

Categories or keywords, the same as the BBB search box. Examples: roofing, plumber, dentist, auto repair, or a business name. Each term runs in each location. Leave empty if you only use profile URLs.

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

City and state, state, or ZIP / postal code. Examples: Austin, TX; 10001; Toronto, ON. Leave empty to search without a location.

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

USA or Canada. Profile URLs work for both, whatever you pick here.

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

Stops each term + location pair after this many businesses. BBB shows at most 225 results per search, so split big areas by city or ZIP code.

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

Stops the whole run after this many business rows.

## `profileUrls` (type: `array`):

BBB business profile links, for example https://www.bbb.org/us/tx/austin/profile/roofing-contractors/fiducia-roofing-restoration-0825-90019908. Review and complaint sub-pages work too. A bad or missing profile gets a free status row.

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

Order BBB uses for search results.

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

Keep only BBB Accredited Businesses. Skipped businesses are not charged.

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

Keep only businesses with this BBB letter grade or better. Not rated (NR) businesses are dropped when this is set. Skipped businesses are not charged.

## `includeCounts` (type: `boolean`):

Adds the number of public customer reviews and complaints for each business, and the average review stars when the business has 15 reviews or fewer. Two small requests per business.

## `includeProfileDetails` (type: `boolean`):

Tries each BBB profile page for website, accreditation date, years in business and closed-complaint counts. bbb.org often refuses these pages to cloud servers. When it does, the row keeps search data (detailLevel = search) and you pay the same business price. After 3 refusals the run stops trying.

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

Adds one row per public customer review: stars, date, text and business reply. Reviewer names are left out. Charged per row at the lower review/complaint price.

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

Newest first. BBB serves 15 per page.

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

Adds one row per public complaint: type, status, date, text and the business response. BBB masks personal details in complaint text. Charged per row at the lower review/complaint price.

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

Newest first. BBB serves 15 per page.

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

Leave off to use the automatic fallback below. If you set a proxy here, it is used for every request and the fallback is off.

## `autoProxyFallback` (type: `boolean`):

Starts without a proxy. If bbb.org blocks or challenges requests, switches to Apify datacenter proxy, then residential. Proxy cost is included in the price.

## `allowResidentialFallback` (type: `boolean`):

Turn off to never use residential proxy. Profile details may then fall back to search data only.

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

Requests are still spaced out. Higher is faster and more likely to be rate-limited.

## Actor input object example

```json
{
  "searchTerms": [
    "roofing"
  ],
  "locations": [
    "Austin, TX"
  ],
  "country": "USA",
  "maxResultsPerSearch": 10,
  "maxResults": 1000,
  "sort": "Relevance",
  "accreditedOnly": false,
  "minRating": "",
  "includeCounts": true,
  "includeProfileDetails": false,
  "includeReviews": false,
  "maxReviewsPerBusiness": 15,
  "includeComplaints": false,
  "maxComplaintsPerBusiness": 15,
  "autoProxyFallback": true,
  "allowResidentialFallback": true,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

All rows. Filter by the type field.

## `runSummary` (type: `string`):

Status per input, profile pages read or refused, requests, kB per 1,000 businesses and proxy tier used.

# 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": [
        "roofing"
    ],
    "locations": [
        "Austin, TX"
    ],
    "maxResultsPerSearch": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/bbb-business-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": ["roofing"],
    "locations": ["Austin, TX"],
    "maxResultsPerSearch": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("dottti/bbb-business-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": [
    "roofing"
  ],
  "locations": [
    "Austin, TX"
  ],
  "maxResultsPerSearch": 10
}' |
apify call dottti/bbb-business-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dottti/bbb-business-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/OiOJbiiSVWKW16k6d/builds/r057OcesbO6EHh2il/openapi.json
