# 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
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## BBB.org Business Profile Scraper — Ratings, Reviews, Complaints & B2B Leads

Scrape business profiles from the **Better Business Bureau** (bbb.org) across
the USA and Canada. Every business is one record: BBB rating, accreditation,
contact and lead-gen data (decoded email, phone, website, social profiles,
owner), address with coordinates, business type, years in business, licenses,
hours, and category tree — plus opt-in customer reviews and complaints.

### What you can do

- **Search by business term + location** — e.g. "plumber" in "Los Angeles, CA"
  or "Toronto, ON" — with filters for accredited-only and distance.
- **Paste BBB URLs** — mix business profile pages
  (`https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178`)
  and search result pages in one list.
- **Scrape reviews & complaints** — optional per-business surcharge to also
  pull customer reviews (rating, author, text, date, response) and complaints
  (type, status, date, resolution), independently paginated.
- **Resume & recurring updates** — turn on Incremental mode to get only NEW,
  UPDATED, and REAPPEARED businesses on every scheduled run, or resume one
  specific interrupted crawl with `resumeFromRunId`.

### Input

| Field | Description |
|---|---|
| **Mode** | `search` (term + location) or `url` (paste BBB links). |
| **Business terms** | Keyword terms to search (search mode). Each is combined with the location. |
| **Location** | City + state/province or ZIP code. Leave empty for nationwide. |
| **Country** | `USA` or `CAN`. |
| **Sort order** | `Relevance`, `Rating`, `Distance`, or `AToZ`. Applied by BBB itself. |
| **Search radius (miles)** | Limit results within N miles of the location. |
| **Accredited businesses only** | Only return BBB-accredited businesses. |
| **BBB URLs** | Paste BBB search or business-profile URLs (url mode). Auto-detected and mixed. |
| **Max businesses** | Run's cap on how many business profiles to return. Default 20; `0` = unlimited. |
| **Max pages per search / URL** | Result pages walked per term/URL (15 per page). Empty = unlimited — stops at Max businesses or end of results. |
| **Scrape customer reviews** | Also pull each business's reviews (adds the reviews & complaints surcharge). |
| **Scrape complaints** | Also pull each business's complaints (adds the reviews & complaints surcharge). |
| **Max review pages per business** | Cap on review pages (default 5). |
| **Max complaint pages per business** | Cap on complaint pages (default 5). |
| **Resume from a previous run** | Continue one specific previous run/dataset — businesses already collected there are skipped. |
| **Incremental changes for scheduled runs** | Daily/recurring monitoring. First run returns everything as `NEW`; later runs return only changes. |
| **State key** | Optional name for a monitoring campaign. Auto-derived from your search settings when left empty. |
| **Emit unchanged businesses** | Also return unchanged businesses, marked `UNCHANGED`. Bills extra rows. |
| **Emit expired businesses** | Also return businesses no longer found, marked `EXPIRED`. Bills extra rows. |
| **Proxy** | Apify Proxy — residential recommended for maximum reliability. |
| **Pipe results into your apps** | Optional MCP connectors — see below. |
| **Notion parent page** | Notion page under which item pages are created (Notion connector only). |
| **Max items to export per connector** | Cap on items written to each connector per run. Does not affect the dataset. |

### Output

One record per business profile. Example:

```json
{
  "name": "Example Plumbing Co.",
  "alternateNames": ["Example Plumbing Co., LLC"],
  "businessId": "0825-1000223803",
  "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",
  "bbbFileOpenedDate": "2024-01-16",
  "phone": "(555) 010-0100",
  "additionalPhones": null,
  "email": "info@example.com",
  "additionalEmails": null,
  "website": "https://www.example.com/",
  "ownerName": "Jane Doe",
  "ownerTitle": "Owner/Manager",
  "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"],
  "categoryTree": [{"name": "Plumber", "url": "https://www.bbb.org/us/tx/austin/category/plumber"}],
  "businessType": "Limited Liability Company (LLC)",
  "yearsInBusiness": 3,
  "businessStartDate": "2023-03-14",
  "incorporatedDate": "2023-03-14",
  "numberOfEmployees": null,
  "isOutOfBusiness": false,
  "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,
  "complaintsClosedLast3Years": 0,
  "complaintsClosedLast12Months": 0,
  "complaintResolutionRate": null,
  "socialProfiles": null,
  "reviews": null,
  "complaints": null,
  "logoUrl": "https://m.bbb.org/prod/ProfileImages/2024/1f7a8bd4-fdd2-45ca-8963-5c246e1d3b85.png",
  "reviewsUrl": "https://www.bbb.org/us/tx/austin/profile/plumber/example-plumbing-co-0825-1000000000/customer-reviews",
  "complaintsUrl": "https://www.bbb.org/us/tx/austin/profile/plumber/example-plumbing-co-0825-1000000000/complaints",
  "scrapedAt": "2026-01-01T00:00:00Z",
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-01-01T00:00:00Z",
  "lastSeenAt": "2026-01-01T00:00:00Z"
}
```

`email` is returned ready to use: BBB publishes addresses in a scrambled form to
deter harvesting, and this scraper returns the plain address. It is `null` when
the business has not published one — a third party's address that happens to
appear on the page is never returned in its place.

`complaintResolutionRate` is `complaintsClosedLast3Years / complaintsTotal` —
BBB publishes closed-complaint counts, not a separate "resolved" figure. It is
`null` when a business has no complaints on file.

With `scrapeReviews` / `scrapeComplaints` on, `reviews` and `complaints` carry
the full items:

```json
{
  "reviews": [
    {
      "id": "0825_10000000_000000",
      "author": "J. D.",
      "rating": 1.0,
      "date": "2026-07-24",
      "text": "…",
      "businessResponse": null,
      "businessResponseDate": null
    }
  ],
  "complaints": [
    {
      "id": "0825_10000000_000001",
      "type": "Billing Issues",
      "status": "Resolved",
      "date": "2026-07-07",
      "text": "…",
      "responses": [{"type": "business", "date": "2026-07-09", "text": "…"}],
      "responseCount": 1
    }
  ]
}
```

**Incremental mode fields:**
| Field | Description |
|---|---|
| `changeType` | `NEW` / `UPDATED` / `UNCHANGED` / `REAPPEARED` / `EXPIRED` |
| `changedFields` | Top-level fields that changed since last seen |
| `firstSeenAt` | When this business was first observed by this monitoring campaign |
| `lastSeenAt` | When this business was last observed |

### Reviews & complaints enrichment

When `scrapeReviews` or `scrapeComplaints` is on, each business carries a
`reviews` and/or `complaints` array, paginated up to your page caps (BBB serves
about 10 per page, and the walk stops at the last page BBB reports).

The **detail-enrichment** surcharge is one unit per business whose review /
complaint pages were actually fetched and returned. A business that is
suppressed by incremental mode (UNCHANGED with Emit unchanged off) is never
charged, and neither is one whose detail pages could not be read.

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

Businesses can be pushed straight into the tools you already use, over the
Model Context Protocol — no glue code and no separate integration run.

1. In Apify, open **Settings → API & Integrations** and authorize a connector
   (Notion, Linear, Airtable, or Apify).
2. Select it under **`mcpConnectors`** in this actor's input.
3. For Notion, also set **`notionParentPageUrl`** to the page (URL or id) the
   business pages should be created under — the Notion export stays off until
   this is filled in. Other connectors ignore it.
4. Use **`maxNotifyListings`** to cap how many businesses are written to each
   connector per run (default 50).

Notion receives a rich page per business; the other connectors get a
best-effort write or digest. The export runs after the crawl, never changes the
dataset, and never changes what you are billed — leave `mcpConnectors` empty to
skip it entirely.

| Field | Description |
|---|---|
| `mcpConnectors` | Connectors to send results to. Empty = skip. |
| `notionParentPageUrl` | Parent Notion page for created item pages. Notion only. |
| `maxNotifyListings` | Max businesses written per connector per run (default 50). |

### Pricing

Three charge events:

| Event | Description |
|---|---|
| `apify-actor-start` | One unit charged at startup (billed per GB of memory). |
| `apify-default-dataset-item` | One unit per business profile returned. |
| `detail-enrichment` | One unit per business when reviews and/or complaints are scraped. |

Tiered pricing: FREE / BRONZE / SILVER / GOLD / PLATINUM / DIAMOND.

### Proxy

BBB requires a browser-grade connection. **A residential Apify Proxy session
is recommended** for maximum reliability. A datacenter connection may work
for a small subset of requests but will have significantly lower reliability.
The scraper automatically handles session management and rotation.

### Resume & recurring updates

- **`resumeFromRunId`** — paste a previous run ID or dataset ID to continue
  that interrupted crawl. Businesses already in that run are skipped and
  not billed again.
- **`incrementalMode`** — turn on for scheduled daily monitoring. The actor
  remembers the previous run's state (keyed by your search settings) and
  returns only changes. Use `stateKey` to name a campaign; leave empty to
  auto-derive.

The two fields target different workflows: Resume recovers one broken run;
Incremental mode powers recurring monitoring without you pasting IDs every time.

# 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.

## `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.

## `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. 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 a keyword search, a pasted search URL, and a pasted profile URL.

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

Only used when mode = url — ignored in search mode. Paste BBB search-result pages (https://www.bbb.org/search?... ) or direct business-profile URLs (https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178 ). Search 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. Use 0 for unlimited.

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

Maximum result pages walked per search term / pasted search URL (15 businesses per page). Leave empty for unlimited — the run then stops at Max businesses or when a page repeats no new businesses.

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

Apify Proxy is used by default. bbb.org is served to normal datacenter connections; a residential connection is recommended for maximum reliability.

## `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.

## `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.

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

Cap on complaint pages pulled per business (~10 complaints per page). Only used when "Scrape complaints" is on.

## `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.

## `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,
  "startUrls": [
    "https://www.bbb.org/us/ca/los-angeles/profile/plumber/fords-plumbing-1216-100081178"
  ],
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "scrapeReviews": false,
  "scrapeComplaints": false,
  "maxReviewPages": 5,
  "maxComplaintPages": 5,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "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"],
    "proxy": { "useApifyProxy": True },
    "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"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "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/feqNab1NhbFhyVSPS/openapi.json
