# BBB Scraper - Business Ratings, Reviews, Complaints & B2B Leads (`abotapi/bbb-org-scraper`) Actor

Scrape bbb.org (Better Business Bureau, USA & Canada) business profiles: BBB rating, accreditation, contact and lead-gen data (decoded email, phone, website, socials, owner), address + coordinates, business type, years in business, licenses, hours, plus opt-in customer reviews and complaints..

- **URL**: https://apify.com/abotapi/bbb-org-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Lead generation, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 35 total users, 16 monthly users, 83.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 business results

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/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.org Scraper: Business Profiles, Ratings, Reviews and Complaints

BBB.org Scraper turns Better Business Bureau (BBB) into structured lead and reputation data for the USA and Canada. Get the BBB rating, accreditation status, decoded contact details, address with coordinates, licenses, hours, category tree, and years in business for every profile, plus each business's own opt-in customer reviews and complaints. Search by business term and location or paste BBB links, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Two ways to find businesses.** Search by business term plus location, or paste BBB search-result, category and business-profile links, mixed in one run.
- **Past BBB's 225-result view.** BBB shows at most 15 pages per search however many businesses match; **Maximize coverage** walks the same search in BBB's other sort orders to reach the businesses the first order never shows, each still returned and billed once.
- **Decoded contact details.** BBB scrambles published email addresses against harvesters; this actor returns the plain address, and leaves it empty when the business truly has not published one.
- **Full profile depth.** Rating, accreditation, owner, address with coordinates, business type, years in business, licenses, operating hours, service areas and the full category tree.
- **Optional reviews and complaints.** Turn on a per-business add-on to also pull customer reviews and complaints, each paginated up to a per-business page cap you set (0 walks every page).
- **Resume or monitor on a schedule.** Continue one interrupted crawl by run id, or turn on Incremental mode so recurring runs return only new and changed businesses.
- **Built to fail loudly, not quietly.** A run that could not read BBB at all stops with a clear error instead of reporting an empty result as if nothing was found.

### Use Cases

- **Lead generation:** pull business name, decoded email and phone, website, owner and address for every plumber, roofer or HVAC company in a city.
- **Reputation monitoring:** track a business's BBB rating, accreditation, complaint volume and resolution rate over time.
- **Compliance and licensing checks:** collect license numbers, issuing agency and expiration dates alongside years in business and entity type.
- **Customer feedback analysis:** read customer reviews and complaints together with the business's own responses.
- **Recurring portfolio monitoring:** schedule the same search or URL list and use Incremental mode to see only what changed since the last run.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `businessId` | `"0825-1000000000"` |
| `name` / `alternateNames` | `"Example Plumbing Co."` / `["Example Plumbing Co., LLC"]` |
| `profileUrl` | BBB link to the business profile |
| `bbbRating` / `ratingScore` | `"A+"` / `97.0` (BBB's 0-100 score behind the grade; search results only, `null` when unrated) |
| `isAccredited` / `accreditedDate` / `accreditationRevokedDate` | `true` / `"2024-01-16"` / `null` (the date BBB revoked accreditation, if it did) |
| `phone` / `additionalPhones` | `"(555) 010-0100"` / `null` |
| `email` / `additionalEmails` | `"info@example.com"` / `null` |
| `website` / `additionalWebsites` / `isHeadquarters` | `"https://www.example.com/"` / further websites the business published (`null` when none) / `false` (`true` when this listing is the head office) |
| `ownerName` / `ownerTitle` / `contacts` | `"Jane Doe"` / `"Owner/Manager"` / every contact BBB lists, `[{"name", "title", "isPrincipal", "isManagement"}]` (`null` when none) |
| `formattedAddress` | `"100 Example St, Austin, TX 78701"` |
| `latitude` / `longitude` | `30.27` / `-97.74` |
| `serviceAreas` / `serviceAreaDescription` | `["Travis County, TX"]` / `"Austin and surrounding areas"` (the business's own wording, when it gives one) |
| `primaryCategory` / `categories` / `primaryCategoryId` / `categoryIds` | `"Plumber"` / `["Plumber", "Plumbing Renovation"]` / `"10113-000"` / `["10113-000", "10113-300"]` (BBB category codes, usable in the Category filter; search results only) |
| `businessType` / `yearsInBusiness` | `"Limited Liability Company (LLC)"` / `3` |
| `licenses` | number, expiration date and issuing agency (when on file) |
| `hours` | opening and closing time per day (when published) |
| `reviewsTotal` / `averageReviewRating` | `1` / `1.0` |
| `complaintsTotal` / `complaintResolutionRate` | `0` / `null` |
| `socialProfiles` | the business's own linked social accounts (when published) |
| `logoUrl` | link to the business's logo image |
| `alerts` / `governmentActions` | BBB alerts (e.g. `{"title": "Licensing", "text": "...", "typeId": 8111, "severity": 1}`) and government actions (`{"title", "text"}`) on the profile; `null` when none. `ratingNotRatedReason` explains an `NR` grade |
| `acceptsQuoteRequests` / `requestQuoteUrl` / `hasServiceArea` / `leaveReviewUrl` | `true` / BBB "Get a Quote" form link (`null` when the business takes no quote requests) / `true` (search results only) / BBB link to leave a review |
| `searchKeyword` / `searchLocation` / `localProfileUrl` | `"plumber"` / `"Austin, TX"`: which of your searches found the business (`null` for a pasted profile link) / BBB link to the local branch your search matched, for businesses with several locations (`profileUrl` is then the head office; `null` when BBB lists no separate branch) |
| `scrapedAt` | `"2026-01-01T00:00:00Z"` |
| `changeType` | `"NEW"` (Incremental mode only, see below) |

With **Scrape customer reviews** and/or **Scrape complaints** turned on, the same business record also carries a `reviews` array (one entry per review with `id`, `author`, `rating`, `date`, `text`, `businessResponse`, `businessResponseDate`, `businessRebuttal` and `customerResponse`) and/or a `complaints` array (one entry per complaint with `id`, `type`, `status`, `date`, `text`, `responses` and `responseCount`); both stay `null` when the corresponding option is off, and empty arrays turn into `null` too when a business genuinely has none on file.

Besides the default **Businesses** table, the dataset's **Output** tab offers four more views: **Detail / leads** (contact, owner, address, geo, licenses and hours in one row), **Reviews** and **Complaints** (one row per review/complaint, unwound from the arrays above), and **Changes since the last run** (`changeType` plus `changedFields`, for Incremental mode runs). These only reshape how the same dataset rows are read; they add no new items or charges.

### How to Use

1. Pick a **mode**: `search` (business term plus location) or `url` (paste BBB links).
2. Fill in the fields for that mode, and add filters such as accredited-only or a search radius.
3. Optionally turn on **Scrape customer reviews** and/or **Scrape complaints**, and set their page caps.
4. Set **Max businesses** to control run size and cost, then click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by business term and location:**

```json
{
  "mode": "search",
  "searchTerms": ["plumber"],
  "location": "Los Angeles, CA",
  "country": "USA",
  "maxItems": 20
}
```

**Paste a BBB profile link:**

```json
{
  "mode": "url",
  "startUrls": [
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178"
  ],
  "maxItems": 10
}
```

**Search plus reviews and complaints:**

```json
{
  "mode": "search",
  "searchTerms": ["hvac"],
  "location": "Toronto, ON",
  "country": "CAN",
  "scrapeReviews": true,
  "scrapeComplaints": true,
  "maxItems": 15
}
```

**Recurring monitoring of one search:**

```json
{
  "mode": "search",
  "searchTerms": ["roofing"],
  "location": "Denver, CO",
  "incrementalMode": true,
  "stateKey": "denver-roofing"
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/bbb-org-scraper").call(run_input={
    "mode": "search",
    "searchTerms": ["plumber"],
    "location": "Los Angeles, CA",
})
for business in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(business["name"], business["bbbRating"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/bbb-org-scraper').call({
  mode: 'search',
  searchTerms: ['plumber'],
  location: 'Los Angeles, CA',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Search by plain names, or paste any BBB link

**Search mode** already takes plain text: a business term and a city/state ("plumber" / "Los Angeles, CA"), never a raw BBB id or slug. BBB's own free-text search resolves the name for you.

**URL mode** auto-detects every shape below from a pasted link; mix supported shapes freely in one `startUrls` list.

Running URL mode without editing `startUrls`, or omitting `startUrls` via the API, now scrapes one example of each supported shape (USA profile, Canada profile, `/customer-reviews`, `/complaints`, search results and a city category page) instead of nothing. Pass your own `startUrls` to replace them.

| URL shape | Example | Supported? |
| --- | --- | --- |
| USA profile | `https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178` | ✅ |
| Canada profile | `https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951` | ✅ |
| Profile, trailing slash | `…/fords-plumbing-1216-100081178/` | ✅ |
| Profile + `/customer-reviews`, `/complaints` or `/details` | `…/fords-plumbing-1216-100081178/customer-reviews` | ✅ (scraped as the same business; reviews/complaints are still fetched correctly even from a suffixed link) |
| Profile + `?query` / tracking params / `#fragment` | `…/fords-plumbing-1216-100081178?utm_source=newsletter` | ✅ (query, tracking params and fragment are all ignored) |
| Bare `bbb.org` host (no `www.`) | `https://bbb.org/us/ca/los-angeles/profile/…` | ✅ |
| Search-result page, with filters / `page=N` | `https://www.bbb.org/search?find_text=plumber&find_loc=Los+Angeles%2C+CA&filter_category=plumber&page=2` | ✅ (walked across its result pages) |
| `m.bbb.org` (mobile host) | `https://m.bbb.org/us/ca/.../profile/...` | ❌ (BBB has no mobile subdomain; every path tested 404s. Logged as "unrecognized" and skipped.) |
| Branch link with `/addressId/<n>` (the `localProfileUrl` shape) | `https://www.bbb.org/us/ca/los-angeles/profile/plumber/roto-rooter-plumbing-water-clean-up-1216-1000052192/addressId/48389` | ✅ (scraped as the business the link belongs to, from its main profile) |
| Category page (city, state, country or Canada) | `https://www.bbb.org/us/ca/los-angeles/category/plumber`, `https://www.bbb.org/us/ca/category/plumber`, `https://www.bbb.org/ca/on/toronto/category/plumber` | ✅ (walked across its result pages like a search; `searchKeyword`/`searchLocation` come from the link, e.g. "plumber" / "Los Angeles, CA") |
| "Near me" page | `https://www.bbb.org/near-me/plumber` | ❌ (BBB fills its location from the visitor's connection, so the link has no fixed place. Skipped with a warning; paste the city category link or use Search mode with a Location.) |
| City / state landing pages | `https://www.bbb.org/us/ca/los-angeles` | ❌ (directory pages with no business rows. Logged as "unrecognized" and skipped.) |
| give.org charity profiles | `https://www.give.org/charity-reviews/…` | ❌ (a different site, BBB Wise Giving Alliance, out of scope for this business-profile actor. Logged as "unrecognized" and skipped.) |

An unsupported link is never a silent empty result: it is logged as `Skipping unrecognized URL` so you know to drop it or switch to Search mode, and every other URL in the same run is still processed.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset id and businesses already collected there are skipped and not billed again.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search or URL scope. Each business is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), or `EXPIRED`. `EXPIRED` is only produced once a run has scanned the full tracked scope with `emitExpired` on and neither Max businesses nor Max pages capped it, and never on a run that used Resume. `REAPPEARED` only appears for a business a previous complete run already marked `EXPIRED`, if that same business turns up again on a later run; it is not something an ordinary rerun produces on its own. `stateKey` names or shares one monitoring campaign's state. With Incremental mode off, output is exactly as before.

### Send results into your apps (MCP connectors)

Businesses can be pushed straight into the tools you already use, over Model Context Protocol (MCP) connectors. This is an extra delivery step after the crawl: the Apify dataset and your bill are never changed by it.

1. Authorize a connector once under **Apify, Settings, API & Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it under **Pipe results into your apps** (`mcpConnectors`) in this actor's input.
3. For Notion, also set **Notion parent page** (`notionParentPageUrl`) to the page under which business pages should be created; the Notion export stays off until this is filled in. Other connectors ignore it.
4. Use **Max items to export per connector** (`maxNotifyListings`) to cap how many businesses are written to each connector per run.

Notion receives one rich page per business; other connectors get a best-effort write, or a plain-text digest if no write tool is available. Leave `mcpConnectors` empty to skip this step entirely.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (business term plus location) or `url` (paste BBB links). |
| `searchTerms` | array | none (prefill: `["plumber"]`) | Business terms to search. Search mode only. API callers may also send `search` (one term) or `searches` (a list); both are merged into `searchTerms`, duplicates removed. |
| `search` | string | none | API alias: one business term, merged into `searchTerms`. Search mode only. |
| `searches` | array | none | API alias: list of business terms, merged into `searchTerms`. Search mode only. |
| `location` | string | none (prefill: `"Los Angeles, CA"`) | City plus state/province, or a ZIP/postal code. Empty searches nationwide. |
| `country` | string | `USA` | `USA` or `CAN`. |
| `locations` | array | none | Extra locations; every business term is searched in `location` and in each of these. A business found more than once is returned (and billed) once. Search mode only. |
| `sort` | string | `Relevance` | `Relevance`, `Rating`, `Distance`, `AToZ` or `ZToA`, applied by BBB itself. |
| `distance` | integer | none (BBB's own default radius applies) | Limit results to within this many miles of the location, using BBB's own distance filter. |
| `accreditedOnly` | boolean | `false` | Only return BBB-accredited businesses from search results and pasted search-result links; a directly pasted profile link is always returned regardless of accreditation. |
| `minRating` | string | none (every rating) | Minimum BBB letter grade, `A+` to `F`; unrated (`NR`) businesses are skipped. Same scope as `accreditedOnly`. |
| `category` | string | none | One BBB category, by name (`"Commercial Plumber"`) or BBB category code (`"10113-100"`). Search mode only. |
| `stateFilter` | string | none | 2-letter state/province code applied by BBB, most useful for a nationwide search. Search mode only. |
| `requestQuoteOnly` | boolean | `false` | Only businesses that accept "Get a Quote" requests through BBB. Search mode only. |
| `servesMyAreaOnly` | boolean | `false` | Only businesses BBB lists as serving the searched location. Search mode only. |
| `onlyWithComplaints` | boolean | `false` | Only businesses with at least one complaint on file. Read from each profile, so skipped businesses are not billed but a run may return fewer than Max businesses. Search and URL mode. |
| `startUrls` | array | 6 sample links, one per supported shape (same as the prefill) | BBB search-result, category and/or business-profile links (USA or Canada), mixed. See [Search by plain names, or paste any BBB link](#search-by-plain-names-or-paste-any-bbb-link) for every supported/unsupported shape. URL mode only. |
| `maxItems` | integer | `20` | Hard cap on businesses returned for the whole run. Maximum `100000`; `0` = up to the maximum of 100000. |
| `maxPagesPerQuery` | integer | none (empty = up to 100 pages) | Result pages walked per search term or search/category URL (15 businesses per page). Empty or `0` = up to 100 pages, stopping earlier at BBB's own last page; values above 100 are treated as 100. |
| `maximizeCoverage` | boolean | `false` | When BBB cuts a search off at 15 pages (225 businesses), also walk it in BBB's other sort orders (A-Z, Z-A, Rating, Distance) to reach more of the listed businesses. Each business is returned and billed once. |
| `proxy` | object | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` | Apify Proxy configuration. Residential is the default, since it is needed for reliability against bbb.org; if Residential is refused for your account, the run automatically attempts a backup connection; some or all pages may still be unavailable. |
| `scrapeReviews` | boolean | `false` | Also collect each business's customer reviews. Adds a per-business charge plus one charge per started block of 10 reviews delivered. |
| `scrapeComplaints` | boolean | `false` | Also collect each business's complaints. Adds a per-business charge. |
| `maxReviewPages` | integer | `5` | Cap on review pages per business. `0` = up to 100 pages, stopping earlier at BBB's last reported page; values above 100 are treated as 100. |
| `maxComplaintPages` | integer | `5` | Cap on complaint pages per business. `0` = up to 100 pages, stopping earlier once every complaint BBB counts has been collected; values above 100 are treated as 100. |
| `maxReviewsPerBusiness` | integer | `20` | Cap on reviews per business, on top of `maxReviewPages`; whichever is hit first stops. Also caps the per-10-reviews charge. Maximum `1000`; `0` = up to the maximum of 1000. |
| `maxComplaintsPerBusiness` | integer | `20` | Cap on complaints per business, on top of `maxComplaintPages`; whichever is hit first stops. Maximum `1000`; `0` = up to the maximum of 1000. |
| `resumeFromRunId` | string | none | Continue one specific interrupted run by its run or dataset id. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring; returns only new and changed businesses on later runs. |
| `stateKey` | string | none (auto-derived from the search/URL and detail settings) | Name or share one monitoring campaign's state. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) businesses unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Also return (and bill) businesses no longer found, marked `EXPIRED`. Only produced on a complete, uncapped, non-resumed scan. |
| `ignoreFieldsForChanges` | array | none | Output fields (or dotted nested paths like `complaints.respondedAt`) that should not count as a change in Incremental mode. `searchKeyword`/`searchLocation`/`localProfileUrl` never count. |
| `mcpConnectors` | array | none (skip) | Optional: send a summary of each business to apps authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which business pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on businesses written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "name": "Example Plumbing Co.",
  "alternateNames": ["Example Plumbing Co., LLC"],
  "businessId": "0825-1000000000",
  "bbbId": "0825",
  "bbbOfficeName": "BBB Heart of Texas",
  "profileUrl": "https://www.bbb.org/us/tx/austin/profile/plumber/example-plumbing-co-0825-1000000000",
  "bbbRating": "A+",
  "isAccredited": true,
  "accreditedDate": "2024-01-16",
  "phone": "(555) 010-0100",
  "email": "info@example.com",
  "website": "https://www.example.com/",
  "ownerName": "Jane Doe",
  "ownerTitle": "Owner/Manager",
  "contacts": [{"name": "Jane Doe", "title": "Owner/Manager", "isPrincipal": true, "isManagement": true}],
  "requestQuoteUrl": "https://www.bbb.org/heartoftexas/quote/request-example-plumbing-co-1000000000",
  "isHeadquarters": false,
  "streetAddress": "100 Example St",
  "city": "Austin",
  "stateCode": "TX",
  "zipCode": "78701",
  "formattedAddress": "100 Example St, Austin, TX 78701",
  "countryCode": "US",
  "latitude": 30.27,
  "longitude": -97.74,
  "serviceAreas": ["Travis County, TX", "Williamson County, TX"],
  "primaryCategory": "Plumber",
  "categories": ["Plumber", "Plumbing Renovation", "Commercial Plumber"],
  "businessType": "Limited Liability Company (LLC)",
  "yearsInBusiness": 3,
  "licenses": [
    {
      "licenseNumber": "44648",
      "expirationDate": "2026-11-30",
      "agencyName": "Texas State Board of Plumbing Examiners",
      "agencyPhone": "800-8456584"
    }
  ],
  "hours": [{"day": "M", "hours": [{"opens": "07:00", "closes": "16:00"}], "note": null}],
  "reviewsTotal": 1,
  "averageReviewRating": 1.0,
  "complaintsTotal": 0,
  "complaintResolutionRate": null,
  "socialProfiles": null,
  "reviews": null,
  "reviewsCollected": null,
  "complaints": null,
  "logoUrl": "https://m.bbb.org/prod/ProfileImages/2024/1f7a8bd4-fdd2-45ca-8963-5c246e1d3b85.png",
  "scrapedAt": "2026-01-01T00:00:00Z",
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-01-01T00:00:00Z",
  "lastSeenAt": "2026-01-01T00:00:00Z"
}
```

### Plan Requirement

This actor uses pay-per-event pricing: one charge when the run starts, one charge per business profile returned, and one extra charge per business only when reviews and/or complaints scraping is turned on. When reviews are scraped, each business is also charged one more reviews & complaints event per started block of 10 reviews delivered on it. Businesses suppressed by Incremental mode, or whose detail pages could not be read, are never billed the extra charges. BBB.org sits behind a bot challenge that plain datacenter connections do not pass, so the default Residential proxy setting is the one to keep; if your Apify plan does not include Residential, the actor automatically attempts a backup connection, but some or all pages may still come back unavailable; check the run log, and consider supplying your own proxy or upgrading your plan for reliable results.

### FAQ

#### How much does it cost?

You pay per business profile returned, plus an extra charge per business only when Scrape customer reviews or Scrape complaints is switched on. The **Pricing** tab shows the current rates for your plan. Use **Max businesses** to cap the cost of any run.

**Reviews are charged in blocks of 10.** When Scrape customer reviews is on, every business returned is charged the "Reviews & complaints" (`detail-enrichment`) event once, as before, and additionally one more `detail-enrichment` event per started block of 10 reviews actually delivered on that business: 1 to 10 reviews = 1 extra event, 11 to 20 = 2, and so on. The count is taken from the reviews in the returned row (shown in the `reviewsCollected` column), after **Max reviews per business** and **Max review pages per business** are applied, so you are never charged for reviews you did not receive. A business with no reviews, a business skipped by Incremental mode, a business returned as `UNCHANGED`, or a business whose review pages could not be read this run adds no review blocks. Complaints are not charged per complaint.

The price of the `detail-enrichment` event for your plan is shown on the **Pricing** tab. Worked examples: with the default Max reviews per business of 20, a business with 20 or more reviews gets 20 delivered = 1 per-business enrichment event + 2 review-block events = 3 `detail-enrichment` events at most; if you raise the cap to 50, a business with 50 reviews delivered = 1 + 5 = 6 `detail-enrichment` events, on top of the business result charge.

**Limits that cap review and complaint cost:** Max reviews per business defaults to 20 (at most 2 review-block events per business) and accepts up to 1,000; Max complaints per business defaults to 20 and accepts up to 1,000; Max review / complaint pages per business default to 5, and values above 100 are treated as 100; Max businesses accepts up to 100,000; Max pages per search values above 100 are treated as 100. `0` on any of them means "as many as possible" and resolves to that field's ceiling, never to an unlimited run; for example Max reviews per business `0` delivers at most 1,000 reviews per business, i.e. at most 100 review-block events per business.

#### Is it legal to scrape BBB.org?

This actor collects only publicly available business profile data that BBB itself publishes. You are responsible for how you use it: follow BBB's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution, particularly of customer reviews or complaint text.

#### Can I get only new or changed businesses on a schedule?

Yes. Schedule the actor and turn on **Incremental changes for scheduled runs**. The first run returns everything as `NEW`; later runs return only `NEW` and `UPDATED` businesses by default. Turn on **Emit unchanged** or **Emit expired** only if you also want unchanged or disappeared businesses returned (and billed). `EXPIRED` needs a complete, uncapped scan; `REAPPEARED` only shows up for a business an earlier complete run had already marked `EXPIRED`.

#### What is the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run so you do not pay twice for businesses it already collected. **Incremental mode** is for running the same search or URL list again and again on a schedule and getting only what changed. You can combine them, but only to bootstrap a brand-new monitoring campaign that has no saved state yet: the resumed businesses are seeded into the campaign's baseline as already present. Once a campaign has saved state, a run with Resume set fails with an error telling you to remove it or pick a different **State key**.

#### Why do I get at most 225 businesses for a big search?

BBB itself shows at most 15 pages of 15 businesses for any one search and sort order, even when it reports thousands of matches (7,000+ plumbers in Los Angeles). Turn on **Maximize coverage** to walk the search again in BBB's other sort orders, add more **locations** (nearby cities or ZIP codes), or narrow it with a **Category filter**. A search BBB cut off this way is never treated as a complete scan, so Incremental mode will not mark the businesses beyond the cut as `EXPIRED`.

#### Why did my run fail instead of returning an empty dataset?

If BBB did not return any readable page for the entire run, even after the actor automatically tried every available connection, it stops with a clear error rather than reporting "no businesses found", so a real block is never confused with a genuinely empty search. Try again in a few minutes. If only some pages could not be read, the run still returns what it found, with a warning that the results may be incomplete.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more leads data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>📇 <a href="https://apify.com/abotapi/yellow-pages-au-scraper"><b>Yellow Pages AU</b></a><br>From $1/1K. Scrape business listings from Yellow Pages Australia by type and location...</td><td>📇 <a href="https://apify.com/abotapi/local-ch-scraper"><b>Local.ch</b></a><br>From $1/1K. Pull structured company data from local.ch, including name, address, GPS...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/truelocal-com-au-scraper"><b>TrueLocal AU Directory Listings &amp; Reviews Scraper</b></a><br>Scrape TrueLocal.com.au business listings by keyword, location, or URL. Extract names...</td><td>🧩 <a href="https://apify.com/abotapi/yandex-maps-scraper"><b>Yandex Maps Scraper</b></a><br>Scrape businesses and places from Yandex Maps by search term or URL for research, leads...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/google-maps-scraper"><b>Google Maps Scraper</b></a><br>Extract business data from Google Maps at scale. Get names, addresses, phone numbers...</td><td>📇 <a href="https://apify.com/abotapi/hipages-business-scraper"><b>HiPages Scraper</b></a><br>Scrape business listings from HiPages Australia by category, location, or URL. Extract...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/bbb-org-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

Choose 'search' to run business-term + location searches with filters, or 'url' to scrape pasted BBB search-result pages and business-profile URLs (both are auto-detected and can be mixed).

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

Only used when mode = search — ignored in url mode. Business category or keyword terms to search on BBB, e.g. "plumber", "roofing", "hvac". Each term is combined with the location below and searched separately.

## `search` (type: `string`):

API alias merged into Business terms.

## `searches` (type: `array`):

API alias merged into Business terms.

## `location` (type: `string`):

City + state (US) or city + province (Canada) to search within, e.g. "Los Angeles, CA" or "Toronto, ON". A ZIP / postal code also works. Leave empty for a nationwide search of the selected country. To search several places in one run, add them under "More locations" below.

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

Optional. Extra cities, states/provinces or ZIP/postal codes to search in the same run, e.g. "San Diego, CA", "90210". Every business term is searched in the Location above AND in each of these; a business found by more than one search is returned (and billed) once. Search mode only.

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

Country to search within. Covers the USA and Canada.

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

Order of search results, applied by BBB itself.

## `distance` (type: `integer`):

Limit results to businesses within this many miles of the location (BBB's own distance filter; needs a Location). Leave empty for BBB's default radius.

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

Only return BBB-accredited businesses; non-accredited results are skipped and not billed. Applies to keyword searches (BBB filters those itself) and pasted search-result URLs; a directly pasted profile URL is always returned.

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

Only return businesses rated at least this BBB letter grade (e.g. "B" keeps A+ through B, and drops B- and below). Businesses BBB has not rated (NR) are skipped. Leave empty for every rating. Applies to keyword searches and pasted search-result URLs; skipped businesses are not billed.

## `category` (type: `string`):

Optional. Narrow each search to one BBB business category, by name (e.g. "Commercial Plumber", "Drain Cleaning") or BBB category code (e.g. "10113-100"). A name is matched to BBB's own category filter for that search; if BBB doesn't offer it there, each business's own categories are matched instead. Search mode only.

## `stateFilter` (type: `string`):

Optional 2-letter state or province code (e.g. "TX", "ON") applied by BBB itself. Most useful with an empty Location (a nationwide search). Search mode only.

## `requestQuoteOnly` (type: `boolean`):

Only return businesses that accept "Get a Quote" requests through BBB (BBB's own filter). Search mode only.

## `servesMyAreaOnly` (type: `boolean`):

Only return businesses BBB lists as serving the searched location, including ones based elsewhere (BBB's "Serving my Area" filter). Needs a Location. Search mode only.

## `onlyWithComplaints` (type: `boolean`):

Only return businesses that have at least one complaint on file with BBB (useful for reputation research). BBB's search does not offer this filter, so each business's profile is opened to read its complaint count; businesses without complaints are skipped and not billed, so a run may return fewer than Max businesses. Works in Search and URL mode.

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

Only used when mode = url — ignored in search mode. Paste BBB search-result pages (https://www.bbb.org/search?... , filters and page= all work) or direct business-profile URLs — USA (https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178 ) or Canada (https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951 ), with or without a trailing slash, a /customer-reviews or /complaints suffix, or tracking params. BBB category pages (https://www.bbb.org/us/ca/los-angeles/category/plumber , also state-, country- and Canada-level) work like a search. Search, category and profile URLs are auto-detected and can be mixed in one run.

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

Hard cap on the number of business profiles returned across the whole run. This is the run's cap. Maximum 100,000; 0 = up to the maximum of 100,000.

## `maxPagesPerQuery` (type: `integer`):

Maximum result pages walked per search term / pasted search or category URL (15 businesses per page). Leave empty or use 0 for up to 100 pages: the run then stops at Max businesses, at BBB's own last page (BBB shows at most 15 pages per sort order; see Maximize coverage) or at 100 pages, whichever comes first. Values above 100 are treated as 100.

## `maximizeCoverage` (type: `boolean`):

BBB shows at most 15 pages (225 businesses) per search, whatever the total it reports (e.g. 7,000+ plumbers in Los Angeles). When a search or pasted search/category URL is cut off this way, also walk it in BBB's other sort orders (A-Z, Z-A, Rating, Distance) to reach businesses the first order never shows. Each business is still returned and billed once. Off by default.

## `scrapeReviews` (type: `boolean`):

Also collect each business's customer reviews (rating, author, text, date, business response), paginated up to Max review pages. Adds the reviews & complaints surcharge per business. 💲 Turning this on charges the **detail-enrichment** event (billed as "Reviews & complaints") once per business returned — including businesses that turn out to have none — PLUS one more **detail-enrichment** event per started block of 10 reviews delivered on that business (1–10 reviews = 1, 11–20 = 2, 50 = 5). Limit it with Max reviews per business.

## `scrapeComplaints` (type: `boolean`):

Also collect each business's complaints (type, status, date, text, response count), paginated up to Max complaint pages. Adds the reviews & complaints surcharge per business. 💲 Turning this on charges the **detail-enrichment** event (billed as "Reviews & complaints") once per business returned — including businesses that turn out to have none.

## `maxReviewPages` (type: `integer`):

Cap on review pages pulled per business (~10 reviews per page). Only used when "Scrape customer reviews" is on. 0 = up to 100 pages; values above 100 are treated as 100. The walk still stops earlier at the last review page BBB reports.

## `maxComplaintPages` (type: `integer`):

Cap on complaint pages pulled per business (~10 complaints per page). Only used when "Scrape complaints" is on. 0 = up to 100 pages; values above 100 are treated as 100. The walk still stops earlier once every complaint BBB counts for the business has been collected.

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

Hard cap on reviews collected per business, on top of "Max review pages per business" — whichever limit is hit first stops the walk. Only used when "Scrape customer reviews" is on. Default 20 (raise it for more reviews per business). Maximum 1,000; 0 = up to the maximum of 1,000 (page cap still applies). 💲 Reviews are billed in blocks of 10 delivered reviews: each started block of 10 charges one extra **detail-enrichment** event on top of the per-business charge (e.g. 50 reviews = 5 extra events), so this cap also caps the review charge.

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

Hard cap on complaints collected per business, on top of "Max complaint pages per business" — whichever limit is hit first stops the walk. Only used when "Scrape complaints" is on. Default 20 (raise it for more complaints per business). Maximum 1,000; 0 = up to the maximum of 1,000 (page cap still applies). Complaints are not billed per complaint.

## `proxy` (type: `object`):

Apify Proxy is used by default on a Residential connection, since bbb.org is unreliable on plain datacenter IPs. If Residential is refused for this account (e.g. a free plan), the run automatically attempts a backup connection — some or all pages may still be unavailable; check the run log.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large crawl of businesses without returning or charging for businesses already collected there. Use this after an interrupted run, or when continuing a business list pull in another run. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching businesses as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED businesses. Turn on "Emit unchanged" or "Emit expired" only when you also want those businesses returned (and billed). State is kept separately for each search/URL and reviews/complaints setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL and detail settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return businesses that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return businesses that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max businesses capped it, not when Max result pages capped it, and not when Resume was used. Leave BOTH "Max businesses" and "Max result pages" empty for the run that should detect expiries. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Output field names that should NOT make a business count as UPDATED (incremental mode only). Use a plain field name (e.g. "logoUrl") to ignore a whole top-level field, or a dotted path (e.g. "complaints.respondedAt") to ignore just that nested field inside every item of a list like reviews/complaints/categoryTree. Ignored fields are still returned in the output and never removed from it — this only controls what counts as a change. searchKeyword, searchLocation and localProfileUrl (which of your searches, and which local branch listing, found the business) never count as a change, so a business reached by a different term, location or branch is not reported as UPDATED.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "plumber"
  ],
  "location": "Los Angeles, CA",
  "country": "USA",
  "sort": "Relevance",
  "accreditedOnly": false,
  "requestQuoteOnly": false,
  "servesMyAreaOnly": false,
  "onlyWithComplaints": false,
  "startUrls": [
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178",
    "https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951",
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/customer-reviews",
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/complaints",
    "https://www.bbb.org/search?find_country=USA&find_text=plumber&find_loc=Los+Angeles%2C+CA&page=1",
    "https://www.bbb.org/us/ca/los-angeles/category/plumber"
  ],
  "maxItems": 20,
  "maximizeCoverage": false,
  "scrapeReviews": false,
  "scrapeComplaints": false,
  "maxReviewPages": 5,
  "maxComplaintPages": 5,
  "maxReviewsPerBusiness": 20,
  "maxComplaintsPerBusiness": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `complaints` (type: `string`):

No description

## `changes` (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 = {
    "mode": "search",
    "searchTerms": [
        "plumber"
    ],
    "location": "Los Angeles, CA",
    "startUrls": [
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178",
        "https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951",
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/customer-reviews",
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/complaints",
        "https://www.bbb.org/search?find_country=USA&find_text=plumber&find_loc=Los+Angeles%2C+CA&page=1",
        "https://www.bbb.org/us/ca/los-angeles/category/plumber"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/bbb-org-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 = {
    "mode": "search",
    "searchTerms": ["plumber"],
    "location": "Los Angeles, CA",
    "startUrls": [
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178",
        "https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951",
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/customer-reviews",
        "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/complaints",
        "https://www.bbb.org/search?find_country=USA&find_text=plumber&find_loc=Los+Angeles%2C+CA&page=1",
        "https://www.bbb.org/us/ca/los-angeles/category/plumber",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/bbb-org-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 '{
  "mode": "search",
  "searchTerms": [
    "plumber"
  ],
  "location": "Los Angeles, CA",
  "startUrls": [
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178",
    "https://www.bbb.org/ca/on/toronto/profile/house-cleaning/custom-maids-incorporated-0107-1151951",
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/customer-reviews",
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178/complaints",
    "https://www.bbb.org/search?find_country=USA&find_text=plumber&find_loc=Los+Angeles%2C+CA&page=1",
    "https://www.bbb.org/us/ca/los-angeles/category/plumber"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/bbb-org-scraper --silent --output-dataset

```

## MCP server setup

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