# BBB Scraper (Reviews, Complaints and Grades, No Login) (`locaihost/bbb-reviews`) Actor

Scrape BBB reviews, customer complaints and business profiles: stars, review text, business replies, complaint type and status, BBB grade, accreditation and years in business. Rank any business against its local rivals and get only new reviews on schedules. No login. From $0.50 per 1,000 rows.

- **URL**: https://apify.com/locaihost/bbb-reviews.md
- **Developed by:** [locaihost data](https://apify.com/locaihost) (community)
- **Categories:** Marketing, Lead generation, Business
- **Stats:** 6 total users, 6 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.28 / 1,000 reviews

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 Scraper (Reviews, Complaints and Grades, No Login)

Get Better Business Bureau data for any US or Canadian business as clean JSON or CSV rows: customer reviews with the business's replies, complaints with the whole exchange, and a business row with BBB grade, accreditation, complaint counts, phone and address. No login, no cookies, no account.

- **Check a company before you deal with it:** BBB grade, accreditation date, complaint counts and alerts next to its newest reviews and complaints.
- **Size up a business in one row:** turn on **Add a reputation summary per business** for its complaint trend, how many complaints it answered and resolved, how fast it replies, its top complaint types and its star mix. See [Reputation summary](#reputation-summary).
- **Rank a business against its local competitors:** turn on **Benchmark against local competitors** for its rank among businesses with the same BBB category in the same city, plain-English highlights and the competitor list. See [Competitor benchmark](#competitor-benchmark).
- **Find trade licences about to expire:** set **Licences expiring within** to list businesses whose licence ends in the next 30, 60 or 90 days. See [Licence expiry watch](#licence-expiry-watch).
- **Monitor your own BBB profile:** schedule it daily with `onlyNew` and get only the reviews and complaints filed since the last run.
- **Read what customers complain about:** complaint type, status and the business's response for a competitor, ready to group and count.
- **Build a list of local businesses:** search BBB by trade and city ("plumber" near "Austin, TX", or several cities and ZIP codes at once) and get grade, accreditation, category, phone, address and approximate map position for each. Keep only accredited ones if you like.
- **Feed analysis and dashboards:** one flat row per review or complaint, with the same review columns as our Trustpilot and Sitejabber scrapers.

### Quick start

Paste this into the **JSON** tab of the input and click **Start**:

```json
{"businesses": ["https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"], "maxReviewsPerBusiness": 20, "maxComplaintsPerBusiness": 10}
```

You get one business row, the 20 newest reviews and the 10 newest complaints: 31 rows in about 30 seconds. A link to the business's reviews or complaints page works too.

More examples:

```json
{"searchQueries": ["plumber"], "searchLocation": "Austin, TX", "maxBusinessesPerSearch": 50}
```

```json
{"searchQueries": ["roofing contractor"], "searchLocation": "Denver, CO", "maxBusinessesPerSearch": 20, "detailsForFoundBusinesses": true, "maxReviewsPerBusiness": 10, "maxComplaintsPerBusiness": 10}
```

```json
{"searchQueries": ["electrician"], "searchLocation": "Austin, TX", "searchLocations": ["Round Rock, TX", "78701"], "maxBusinessesPerSearch": 100, "accreditedOnly": true}
```

```json
{"businesses": ["https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"], "maxReviewsPerBusiness": 0, "maxComplaintsPerBusiness": 500, "dateFrom": "90 days"}
```

### What data can I get?

Every row has `type`: `review`, `complaint`, `business`, `summary` (with **Add a reputation summary per business**), `benchmark` (with **Benchmark against local competitors**), or `not-found` (free, says what to change).

| Group | Fields |
|---|---|
| Review | `reviewId`, `rating` (1 to 5), `text`, `publishedAt`, `reviewerName` (the display name BBB shows, like "Robert S") |
| Review replies | `replyText`, `replyPublishedAt` (the business's reply), `reviewerFollowUpText`, `reviewerFollowUpAt`, `businessFollowUpText`, `businessFollowUpAt` |
| Complaint | `complaintId`, `complaintType` (like "Delivery Issues"), `status` (Resolved, Answered, Unanswered and so on), `filedAt`, `text` |
| Complaint exchange | `businessResponseText`, `businessResponseAt` (the first response), `messages` (the whole exchange in order: `from`, `date`, `text`) |
| Business grade | `businessId`, `name`, `bbbRating` (A+ to F, or NR), `isAccredited`, `accreditedSince`, `stars`, `reviewCount` |
| Complaint counts | `complaintCount` (BBB stops counting at 10,000), `complaintsClosedLast12Months`, `complaintsClosedLast3Years`, `alertCount` (government actions and other alerts BBB shows) |
| Business details | `primaryCategory`, `categories`, `phone`, `email` (only if the business publishes one), `address`, `city`, `state`, `zipCode`, `country`, `websiteUrl`, `domain`, `yearsInBusiness`, `businessStartedAt`, `legalEntityType`, `employees`, `isClosed`, `localBbb` |
| Map position | `latitude`, `longitude`: where BBB places the business. BBB rounds many to 2 decimals (about 1 km, often the city centre), so treat them as approximate |
| Licences | `licenses`: each licence BBB lists, with `number`, `agency` (like "Texas State Board of Plumbing Examiners") and `expiresAt`. From the business's own page, so search rows have it only with **Full details for businesses found** |
| Reputation summary | One `summary` row per business: complaint trend, answered and resolved shares, response days, top complaint types, star mix, reply rate. Fields in [Reputation summary](#reputation-summary) |
| Competitor benchmark | One `benchmark` row per business: rank among local competitors, rank and percentile per measure, highlights, competitor list. Fields in [Competitor benchmark](#competitor-benchmark) |
| Licence watch | `licenseExpiresInDays`, `licenseStatus`, `licenseAgencies`, `expiringLicenses` on business rows, with **Licences expiring within**. Fields in [Licence expiry watch](#licence-expiry-watch) |
| Context | `companyName`, `companyDomain`, `reviewUrl`, `complaintsUrl`, `companyUrl`, `source`, `query`, `searchLocation`, `rank`, `input`, `scrapedAt` |

Review rows use the same columns as our Trustpilot and Sitejabber review scrapers (fields BBB doesn't have, like `title` or `reviewerCountry`, are `null`), so you can stack the outputs into one table.

Real rows from one run (2026-10-18, shortened):

```json
[
  {
    "type": "review",
    "site": "bbb",
    "reviewId": "1296_7039385_889440",
    "companyName": "Amazon.com",
    "companyDomain": "amazon.com",
    "rating": 1,
    "text": "Worked for their fulfillment center and I just read a job description that said work in a supported team environment? Never have I been supported in the two years I have been employed with Amazon...",
    "publishedAt": "2026-10-10",
    "replyText": null,
    "reviewUrl": "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385/customer-reviews#1296_7039385_889440"
  },
  {
    "type": "complaint",
    "site": "bbb",
    "complaintId": "1296_7039385_25375731",
    "companyName": "Amazon.com",
    "complaintType": "Delivery Issues",
    "status": "Resolved",
    "filedAt": "2026-10-11",
    "text": "Placed order June 23 for 4 items. Returned all 4 items June 26 through ******* which stopped providing receipts for Amazon returns. Only refunded for 3 items...",
    "businessResponseText": "Hello *****,\n\nI'* ***** ***** from Amazon.com. I'm writing in response to a complaint filed on your behalf by the Better Business Bureau...",
    "businessResponseAt": "2026-10-12",
    "messages": [
      {"from": "business", "date": "2026-10-12", "text": "Hello *****, ..."},
      {"from": "consumer", "date": "2026-10-13", "text": "Hi BBB,\n\nI'm satisfied with the business's resolution to complaint ID ********. ..."}
    ]
  },
  {
    "type": "business",
    "site": "bbb",
    "businessId": "1296-7039385",
    "name": "Amazon.com",
    "bbbRating": "A+",
    "isAccredited": true,
    "accreditedSince": "1996-08-01",
    "stars": 1.11,
    "reviewCount": 4694,
    "complaintCount": 10000,
    "complaintsClosedLast12Months": 17345,
    "complaintsClosedLast3Years": 61018,
    "alertCount": 4,
    "primaryCategory": "Ecommerce",
    "phone": "(888) 280-4331",
    "address": "202 Westlake Ave N Ste 2",
    "city": "Seattle",
    "state": "WA",
    "zipCode": "98109-5264",
    "country": "US",
    "websiteUrl": "https://www.amazon.com",
    "yearsInBusiness": 31,
    "legalEntityType": "Corporation",
    "localBbb": "BBB Great West + Pacific",
    "latitude": 47.620049,
    "longitude": -122.338104,
    "licenses": [{"number": "601720490", "agency": "Washington Secretary of State", "expiresAt": null}],
    "source": "company"
  }
]
```

The dataset has seven views (**Reviews**, **Complaints**, **Businesses**, **Summary**, **Competitor benchmark**, **Licence expiry watch**, **Not found (free)**) and exports to JSON, CSV, Excel or HTML. The run's `SUMMARY` record has pages read, rows by kind, free rows by reason and timing.

### Reputation summary

Other BBB scrapers give you raw rows. Turn on **Add a reputation summary per business** (`"includeSummary": true`) and each business also gets one `summary` row, worked out from the reviews and complaints the run read for it. Nothing extra is fetched for it, and it holds counts only: no consumer names and no review or complaint text.

```json
{"businesses": ["https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"], "maxReviewsPerBusiness": 100, "maxComplaintsPerBusiness": 500, "includeSummary": true}
```

| Field | What it is |
|---|---|
| `bbbRating`, `isAccredited`, `accreditedSince`, `yearsInBusiness`, `businessStartedAt` | BBB grade and accreditation, years in business |
| `stars`, `reviewCount`, `complaintCount` | BBB's own average and totals, for scale |
| `periodFrom`, `periodTo` | The days the numbers cover. `periodTo` is the day of the run |
| `basis` | How many complaints and reviews the numbers come from and how far back, so a small sample is obvious |
| `complaintsRead`, `complaintsFrom`, `complaintsComplete` | Complaints read, the day they are complete from, and whether every complaint BBB shows was read |
| `complaintsByMonth` | The last 12 calendar months, oldest first: `month`, `count`, `partial`. A month the run didn't read back to is `null`, not 0. The current month is month to date |
| `complaintsLast90Days`, `complaintsPrior90Days`, `complaintTrendPct` | Complaints in the last 90 days, the 90 before, and the change in percent. `null` when the read doesn't reach back 180 days |
| `complaintsAnsweredShare` | Share (0 to 1) the business responded to: a public reply, or BBB status Resolved, Answered or Unresolved |
| `complaintsResolvedShare`, `complaintsAnsweredNotResolvedShare`, `complaintsUnresolvedShare` | BBB's own statuses: Resolved; Answered (the business responded, the customer didn't accept or didn't reply); Unresolved or Unanswered |
| `complaintStatusCounts` | Complaints per BBB status, as BBB spells it |
| `medianResponseDays`, `p90ResponseDays` | Days from a complaint being filed to the business's first public reply: median and 90th percentile |
| `topComplaintTypes` | Up to 5 most common complaint types, with counts |
| `reviewsRead`, `reviewsFrom`, `reviewsComplete` | The same for reviews |
| `averageStars`, `starCounts` | Average stars and the count per star (`"1"` to `"5"`) of the reviews read |
| `reviewsRepliedShare`, `reviewsByMonth` | Share of reviews with a business reply, and reviews per month |

Real summary row for Amazon.com from a run with 200 complaints and 50 reviews (2026-10-20, months shortened to the last 3). 200 complaints cover about two weeks for Amazon, so the 90-day trend is `null`; for a business whose whole list was read it is filled in:

```json
{
  "type": "summary",
  "site": "bbb",
  "businessId": "1296-7039385",
  "companyName": "Amazon.com",
  "companyDomain": "amazon.com",
  "bbbRating": "A+",
  "isAccredited": true,
  "accreditedSince": "1996-08-01",
  "yearsInBusiness": 31,
  "businessStartedAt": "1995-07-01",
  "stars": 1.1,
  "reviewCount": 4693,
  "complaintCount": 10000,
  "periodFrom": "2026-10-01",
  "periodTo": "2026-10-20",
  "basis": "Complaints: the newest 200 of 10,000+ (2026-10-01 to 2026-10-16). Reviews: the newest 50 of 4,693 (2026-10-06 to 2026-10-15).",
  "complaintsRead": 200,
  "complaintsComplete": false,
  "complaintsFrom": "2026-10-01",
  "complaintsByMonth": [
    {"month": "2026-08", "count": null, "partial": false},
    {"month": "2026-09", "count": null, "partial": false},
    {"month": "2026-10", "count": 200, "partial": false}
  ],
  "complaintsLast90Days": null,
  "complaintsPrior90Days": null,
  "complaintTrendPct": null,
  "complaintsAnsweredShare": 1,
  "complaintsResolvedShare": 0.39,
  "complaintsAnsweredNotResolvedShare": 0.61,
  "complaintsUnresolvedShare": 0,
  "complaintStatusCounts": {"Resolved": 78, "Answered": 122},
  "medianResponseDays": 2,
  "p90ResponseDays": 6,
  "topComplaintTypes": [
    {"complaintType": "Product Issues", "count": 78},
    {"complaintType": "Service or Repair Issues", "count": 47},
    {"complaintType": "Delivery Issues", "count": 33},
    {"complaintType": "Billing Issues", "count": 24},
    {"complaintType": "Sales and Advertising Issues", "count": 9}
  ],
  "reviewsRead": 50,
  "reviewsComplete": false,
  "reviewsFrom": "2026-10-06",
  "averageStars": 1.04,
  "starCounts": {"1": 49, "2": 0, "3": 1, "4": 0, "5": 0},
  "reviewsRepliedShare": 0,
  "reviewsByMonth": [
    {"month": "2026-08", "count": null, "partial": false},
    {"month": "2026-09", "count": null, "partial": false},
    {"month": "2026-10", "count": 50, "partial": true}
  ],
  "companyUrl": "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385",
  "input": "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385",
  "scrapedAt": "2026-10-20T03:47:54.922Z"
}
```

**Complaint themes (added 2026-10-21).** Every summary row also has `complaintThemesByMonth`, `risingThemes` and `themeSummary`, worked out from the complaints already read (no extra pages):

| Field | What it is |
|---|---|
| `complaintThemesByMonth` | The last 12 months, oldest first: `month`, `types` (complaints per BBB complaint type that month, `null` for months the run didn't read back to) and `partial` |
| `risingThemes` | Complaint types with at least 3 complaints in the last 90 days and at least twice as many as in the 90 days before: `complaintType`, `last90Days`, `prior90Days`. An empty list means none are rising. `null` when the read doesn't reach back 180 days |
| `themeSummary` | One plain sentence, like the one below |

From a real run on a large plumbing company (300 complaints read, 2025-05-23 to 2026-10-11):

```json
{
  "risingThemes": [],
  "themeSummary": "Service or Repair Issues is the most common complaint type (223 of 300 read). No complaint type has at least doubled over the last 90 days.",
  "complaintThemesByMonth": [
    {"month": "2026-08", "types": {"Service or Repair Issues": 20, "Customer Service Issues": 2, "Billing Issues": 2}, "partial": false},
    {"month": "2026-09", "types": {"Billing Issues": 7, "Service or Repair Issues": 16}, "partial": false}
  ]
}
```

Raise **Max complaints per business** to read back at least 180 days, otherwise `risingThemes` stays `null` instead of guessing.

How to read it:

- **The numbers cover only what the run read.** Lists come newest first, so the review and complaint limits set how far back a summary reaches. For a big company, 500 complaints may be a few weeks; for a local business, 100 is usually its whole history (`complaintsComplete: true`). `basis` always says which.
- Items left out of the output by **Star ratings** or **Only with a business response** still count, because the run read them. **Published on or after** sets where the period starts.
- With **Only new since the last run**, the summary covers what is new since the last run, and a business with nothing new gets no summary (and no charge).
- For businesses found by a search, turn on **Full details for businesses found**: without it the run reads no reviews or complaints, so there is nothing to summarise.
- Summary rows count towards **Max rows in total** and the run's maximum cost. A summary is sent once a business's lists are read, so a run that stops at either limit first returns no summary for that business.

### Competitor benchmark

Turn on **Benchmark against local competitors** (`"benchmarkCompetitors": true`) and each business gets one `benchmark` row: where it stands among businesses with the same BBB category in the same city. It works with business links and with searches.

```json
{"businesses": ["https://www.bbb.org/us/az/phoenix/profile/roofing-contractors/elevate-roofing-pros-llc-1126-1000126260"], "benchmarkCompetitors": true, "competitorsToCompare": 10, "maxReviewsPerBusiness": 0, "maxComplaintsPerBusiness": 0, "includeBusinessInfo": false}
```

That input returns the benchmark row only. Leave the limits at their defaults and you also get the business's own reviews, complaints and business row.

```json
{"searchQueries": ["roofing contractor"], "searchLocation": "Phoenix, AZ", "maxBusinessesPerSearch": 5, "benchmarkCompetitors": true, "maxReviewsPerBusiness": 0, "maxComplaintsPerBusiness": 0}
```

How the competitors are chosen: the run searches BBB for the business's primary category near its city, keeps businesses with the same primary category whose BBB link is in the same city, and takes the first **Competitors to compare** (3 to 25, default 10). If the city has fewer than 3, it uses other businesses in the same state and `cohortScope` says so. Fewer than 3 competitors found gives a free `not-found` row instead of a benchmark.

| Field | What it is |
|---|---|
| `rankOverall`, `overallRank`, `overallOf`, `overallPercentile` | Position among the business and its competitors, like `"5 of 11"` (1 is best), and the share of competitors it beats (0 to 100, ties count half). The overall rank averages the percentiles of the measures below |
| `benchmarkMetrics` | One entry per measure: `grade`, `stars`, `complaintsLast12Months`, `answeredShare`, `resolvedShare`, `avgResponseDays`. Each has `value`, `rank` (1 is best), `of`, `percentile`, `betterThan`, `tiedWith`, `worseThan`, `cohortMedian`, `higherIsBetter`. `rank` is `null` when the business has no figure or fewer than 3 businesses do |
| `highlights` | 2 to 4 plain sentences, like "Slower to reply than 8 of 10 local competitors (6 days on average)" |
| `cohort` | The competitors, best overall first: `name`, `url`, `grade`, `stars`, `complaintsLast12Months`, `avgResponseDays`, `isAccredited`, `overallRank` |
| `cohortScope`, `competitorsCompared`, `primaryCategory`, `city`, `state`, `basis` | Where the competitors are, how many, and how the numbers were worked out |

Real row (2026-10-20, shortened to two competitors and three measures):

```json
{
  "type": "benchmark",
  "companyName": "Elevate Roofing Pros LLC",
  "primaryCategory": "Roofing Contractors",
  "cohortScope": "Phoenix, AZ",
  "competitorsCompared": 10,
  "rankOverall": "5 of 11",
  "overallRank": 5,
  "overallPercentile": 55,
  "highlights": [
    "BBB grade A is below 7 of 10 local competitors",
    "Rated higher than 5 of 9 local competitors (5 stars)"
  ],
  "benchmarkMetrics": [
    {"metric": "grade", "value": "A", "rank": 8, "of": 11, "percentile": 15, "betterThan": 0, "tiedWith": 3, "worseThan": 7, "cohortMedian": "A+"},
    {"metric": "stars", "value": 5, "rank": 1, "of": 10, "percentile": 78, "betterThan": 5, "tiedWith": 4, "worseThan": 0, "cohortMedian": 4.94},
    {"metric": "complaintsLast12Months", "value": 0, "rank": 1, "of": 11, "percentile": 65, "betterThan": 3, "tiedWith": 7, "worseThan": 0, "cohortMedian": 0}
  ],
  "cohort": [
    {"name": "BL Roofing LLC", "grade": "A+", "stars": 5, "complaintsLast12Months": 0, "avgResponseDays": null, "isAccredited": true, "overallRank": 1},
    {"name": "Reyes Roofing LLC", "grade": "A+", "stars": 4.94, "complaintsLast12Months": 1, "avgResponseDays": 8, "isAccredited": true, "overallRank": 4}
  ]
}
```

Honest limits:

- Grade, stars and `complaintsLast12Months` (complaints BBB closed in the last 12 months) are BBB's own figures. The answered, resolved and reply-time measures come from each business's newest page of complaints (up to 10), keeping those filed in the last 12 months. Small local businesses often have none, so those three measures are frequently not ranked. `of` says how many businesses had a figure.
- Counts are not adjusted for business size: a large company with more customers has more complaints.
- Competitors are the ones BBB's search returns first (it lists accredited businesses first), not every business in the city.
- Each benchmark reads about 13 pages (the competitors' pages and one to four search pages). A competitor shared by several businesses in one run is read once. Competitors' own rows are not returned unless you turn on **Also return the competitors' business rows** (charged as business rows).
- A run's maximum cost is respected: a benchmark is not started when the remaining budget can't pay for it.
- Benchmarks start on 2026-11-04. Before that date the run says so, charges nothing for it and carries on with the rest of your input.

### Licence expiry watch

BBB lists trade licences (number, issuing body, expiry date) on business pages. Set **Licences expiring within** (`licenseExpiringWithinDays`, 1 to 365) and the run returns only businesses with at least one licence expiring inside that window, or expired in the last 30 days (flagged). Businesses that don't match are not returned and not charged.

```json
{"searchQueries": ["plumber"], "searchLocation": "Austin, TX", "maxBusinessesPerSearch": 50, "licenseExpiringWithinDays": 60}
```

It also works with `businesses` links and with several search locations. It opens each business's own page, so it turns on **Full details for businesses found** for you, and it returns business rows only: reviews, complaints and summaries are skipped.

| Field | What it is |
|---|---|
| `licenseExpiresInDays` | Days until the soonest matching licence expires. Negative means it already expired (up to 30 days ago) |
| `licenseStatus` | `expiring`, or `expired-recently` when a matching licence ended in the last 30 days |
| `licenseAgencies` | The bodies that issued the matching licences |
| `expiringLicenses` | Only the matching licences: `number`, `agency`, `expiresAt`, `expiresInDays` |

`licenses` still lists every licence BBB shows for the business. Real row (2026-10-20, shortened):

```json
{
  "type": "business",
  "name": "Calixto Plumbing",
  "primaryCategory": "Plumber",
  "city": "Austin",
  "state": "TX",
  "licenseExpiresInDays": 41,
  "licenseStatus": "expiring",
  "licenseAgencies": ["Texas State Board of Plumbing Examiners"],
  "expiringLicenses": [{"number": "44648", "agency": "Texas State Board of Plumbing Examiners", "expiresAt": "2026-11-30", "expiresInDays": 41}]
}
```

Limits: only licences BBB shows on the business's page and only those with an expiry date; BBB's list is what the business or the issuing body reported, so check the issuing body before acting on a date. Days are counted from the day of the run.

### How much does it cost?

You pay per review, complaint or business row returned. A reputation summary is charged as one business row ($0.0005 each on the Free and Starter plans), and only when you turn it on. There is no charge for Apify platform usage on top.

| Row | Free and Starter | Scale | Business |
|---|---|---|---|
| Review | $0.50 per 1,000 | $0.375 per 1,000 | $0.275 per 1,000 |
| Complaint | $0.50 per 1,000 | $0.375 per 1,000 | $0.275 per 1,000 |
| Business | $0.50 per 1,000 | $0.375 per 1,000 | $0.275 per 1,000 |
| Reputation summary, complaint themes included (charged as a business row) | $0.50 per 1,000 | $0.375 per 1,000 | $0.275 per 1,000 |
| Competitor benchmark (from 2026-11-04) | $40 per 1,000 | $30 per 1,000 | $22 per 1,000 |

That is $0.50 per 1,000 rows of any kind on the Free and Starter plans, except benchmark rows ($0.04 each on Free and Starter), which read about 13 pages each. The licence expiry watch uses business rows at the usual price. Plus $0.00005 per run start for each GB of memory ($0.0001 at the default 2 GB). **Apify's free $5 monthly credit covers about 10,000 reviews.** Free-plan runs stop at 1,000 rows each; any paid Apify plan removes the cap.

**What's free:** links that are not BBB businesses, searches with no results and unusable inputs (each comes back as a free `not-found` row saying what to change), reviews left out by **Star ratings** or **Only with a business response**, businesses skipped by **Accredited businesses only**, the same business listed twice in a search or found in two search locations, reviews and complaints skipped by `onlyNew`, and business rows whose grade, stars and counts haven't changed under `onlyNew`. To cap a run's spend, set **Maximum cost per run** in the run options, or **Max rows in total** in the input.

### Limits, stated up front

- Only what a logged-out visitor sees. Every review and complaint page on BBB is public, so the run reads each list in order, 10 per page, up to 2,000 pages per list.
- BBB covers the United States and Canada only.
- Search works like BBB's own search box (a business name or a trade, with an optional city and state or ZIP code) and BBB shows at most 225 results per search. To cover a big area, add more cities or ZIP codes in **More search locations**: each search runs in each place. BBB's search ranks accredited businesses first and matches similar spellings, so a name search for a large company that isn't accredited can return other businesses; the run's status message says so, and a business link always works. A search result has what the result card shows: grade, accreditation, category, phone and address, but no review stars or counts. Turn on **Full details for businesses found** to open each business's page as well; its reviews and complaints are then read with your limits too.
- **Star ratings** apply to reviews only. **Published on or after** applies to both lists: they come newest first, so the run stops reading at that date.
- About reviewers it keeps only the display name BBB shows. Complaints have no consumer name field: BBB masks consumers' names and numbers in complaint texts with asterisks, and the rows keep its masking. Consumer names in complaint text are removed: names BBB's masking misses (signed names, "My name is …", the customer's name in a reply's greeting, staff signatures, half-masked names like `***** A *****`) become `[name removed]`. E-mail addresses and phone numbers in reviews and in consumers' complaint messages are replaced with `[contact removed]`. Business replies keep the business's own role addresses and phone numbers; personal-looking e-mail addresses in them are removed. Business rows never include the names of the owners and managers BBB lists.
- BBB sits behind Cloudflare's visitor check, so pages are read in a real browser from US addresses. That makes the first page of a run take a few seconds, and very large businesses read their deep pages more slowly.
- Big requests are split fairly across the run's time limit. If time runs short, the run ends successfully with the rows so far and says what to narrow.

### Only new since last run, and schedules

Turn on **Only new since the last run** and schedule the Actor daily or weekly. Each run returns only reviews and complaints that earlier runs with the same businesses, searches and filters did not, and stops reading a list at the first item it already sent. A run with nothing new charges nothing. Business rows come back only when the grade, stars or counts changed, so a schedule also tracks grade changes. Set up a schedule under **Schedules** in Apify Console.

### Integrations: API, n8n, Make and AI agents

**API.** Run it and get the rows back in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/locaihost~bbb-reviews/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"businesses": ["https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"], "maxReviewsPerBusiness": 0, "maxComplaintsPerBusiness": 50}'
```

Apify's API clients for Python and JavaScript work the same way.

**n8n, Make, Zapier, Google Sheets, Slack, webhooks.** Apify's built-in integrations send each run's dataset wherever you need it, for example each new complaint into a Slack channel.

**AI agents (MCP).** Add this Actor to Claude, Cursor or any MCP client with `https://mcp.apify.com/?tools=locaihost/bbb-reviews`, then ask something like *"What are the most common complaint types against this roofing company on BBB this year, and does it respond?"*

### FAQ

#### How much does it cost to scrape BBB reviews?

$0.50 per 1,000 reviews, complaints or businesses on the Free and Starter plans, less on Scale and Business. Apify's free $5 monthly credit covers about 10,000 reviews.

#### What is the reputation summary, and how far back does it go?

One extra row per business with its complaint trend, the share of complaints it answered and resolved, median days to its first reply, top complaint types, star mix and review reply rate, worked out from the reviews and complaints the run read. Turn on **Add a reputation summary per business**. It goes back as far as the run reads, so raise **Max complaints per business** and **Max reviews per business** for a longer period; its `basis` field says how many items it is based on.

#### What does a competitor benchmark show, and what does it cost?

One row per business with its rank among local competitors that share its BBB category and city, the rank and percentile for grade, stars, complaints and reply times, plain-English highlights and the competitor list. $0.04 per benchmark row on the Free and Starter plans from 2026-11-04. See [Competitor benchmark](#competitor-benchmark).

#### How do I find businesses whose licence is about to expire?

Set **Licences expiring within** to a number of days and run a search such as "plumber" near "Austin, TX". Only businesses with a matching licence are returned. See [Licence expiry watch](#licence-expiry-watch).

#### Can I use it through the API or from Python?

Yes. Use the `run-sync-get-dataset-items` call above, or Apify's Python and JavaScript clients. Every run's data is also available as JSON, CSV or Excel from its dataset.

#### Can I use it from an AI agent?

Yes. Through the Apify MCP server (`https://mcp.apify.com/?tools=locaihost/bbb-reviews`) any MCP client can call it with a list of BBB links or a search and read the rows back.

#### Is it legal to scrape BBB?

The Actor reads only pages any logged-out visitor can open; it never logs in or uses cookies from an account. BBB's own terms restrict automated collection, so check them against your use. It keeps only a reviewer's display name, keeps BBB's own masking in complaints and removes contact details typed into reviews and complaints. Reviews and complaints are written by members of the public: respect copyright when republishing them, follow privacy law (such as GDPR or CCPA) if you store reviewer names, and never use the data to contact, profile or harass reviewers or complainants. You are responsible for how you use the results.

#### How do I find a business's BBB link?

Search for it by name: put the name in **Search** (and its city in **Search near**). Each result's `companyUrl` is the link to use in **BBB business links**. That works well for local businesses. For a large company BBB's search can rank other businesses first (it puts accredited ones on top), so if the results don't show it, search the web for the company name plus "BBB" and paste the bbb.org link.

#### Can I get only accredited businesses, or search many cities at once?

Yes. Turn on **Accredited businesses only** (the others are skipped and cost nothing), and list extra cities or ZIP codes in **More search locations**. A business found in two places is returned and charged once.

#### Why are some fields empty?

In a summary row, a month or trend is `null` when the run didn't read back that far, and the complaint or review fields are `null` when that list wasn't read (its limit was 0). `replyText` is empty when the business didn't reply, and the follow-up fields when there was no further exchange. `title`, `language` and `reviewerCountry` are always `null` because BBB doesn't show them. Search results leave `stars`, `reviewCount` and the complaint counts empty (turn on **Full details for businesses found** to get them). `email` and `websiteUrl` exist only where the business published them on BBB.

### More from locaihost data

- [Trustpilot reviews scraper](https://apify.com/locaihost/trustpilot-reviews): Trustpilot reviews, TrustScore and reply rate, in the same review columns.
- [Sitejabber reviews scraper](https://apify.com/locaihost/sitejabber-reviews): Sitejabber reviews and ratings, in the same review columns.
- [Google Maps business leads](https://apify.com/locaihost/google-maps-leads): local businesses with ratings and contact details from Google Maps.
- [Company enrichment by domain](https://apify.com/locaihost/company-profile): company name, description, logo, socials and role contact addresses from each site.

### Did it help?

If this saved you time, a quick rating on the Store helps other people find it.

### Support

Something not working, or a field you need? Open an issue on the **Issues** tab: we answer issues within 24 hours.

# Changelog

This Actor's version history is a separate document: https://apify.com/locaihost/bbb-reviews/changelog.md

# Actor input Schema

## `businesses` (type: `array`):

Links to businesses on BBB, like https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385. A link to the business's reviews or complaints page works too. To find a business by name, use Search below.

## `searchQueries` (type: `array`):

Search BBB the way its search box does: a business name (Comcast) or a trade (plumber, roofing contractor). Each result is a business row with its BBB grade, accreditation, category, phone and address.

## `searchLocation` (type: `string`):

City and state, or a ZIP or postal code, like Austin, TX or 10001. Leave empty to search the whole country. To search several places, use More search locations.

## `searchLocations` (type: `array`):

More cities or ZIP codes: every search runs in each of them (up to 100), with its own Max businesses per search. BBB shows at most 225 results per search, so several ZIP codes cover a big area better than one city. A business found in two places is charged once.

## `searchCountry` (type: `string`):

BBB covers the United States and Canada.

## `maxBusinessesPerSearch` (type: `integer`):

BBB shows up to 225 results per search (15 pages of 15). 0 means all of them. The same business listed twice is returned once.

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

Searches return only BBB-accredited businesses. The others are skipped and cost nothing. Business links are always read.

## `detailsForFoundBusinesses` (type: `boolean`):

Open each business a search finds: its row then has review stars and counts, complaint counts, years in business and website, and its reviews and complaints are read with the limits below. Slower: one more page per business, plus the review and complaint pages.

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

Customer reviews, newest first, 10 per page. 0 for none. Every review page is public, so a large number like 100000 gets them all (up to 2,000 pages).

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

Complaints with the business's responses, newest first. 0 for none.

## `includeBusinessInfo` (type: `boolean`):

Add one business row per business link: BBB grade, accreditation and since when, review stars and count, complaint counts, category, phone, address, website, years in business. Charged like any other row.

## `includeSummary` (type: `boolean`):

Add one summary row per business, worked out from the reviews and complaints this run reads: complaints per month and the 90-day trend, the share the business answered and resolved, median days to its first reply, top complaint types, complaint types by month and the ones rising, star mix and review reply rate, plus BBB grade and accreditation. Charged as one business row. The numbers cover only what the run reads, so raise the review and complaint limits for a longer period.

## `benchmarkCompetitors` (type: `boolean`):

Add one benchmark row per business: its overall rank among local competitors (like "3 of 11"), its rank and percentile for BBB grade, review stars, complaints closed in the last 12 months, share of complaints answered and resolved, and days to first reply, plus plain-English highlights and the competitor list. Works with business links and with searches. The competitors are found with BBB search and each is read once, so a benchmark reads about 12 pages. The competitors' own rows are not returned unless you turn on Also return the competitors' business rows.

## `competitorsToCompare` (type: `integer`):

How many local competitors each business is compared with, from 3 to 25. Same BBB category and same city; if the city has fewer than 3, other businesses in the state are used and the row says so. More competitors read more pages.

## `includeCompetitorRows` (type: `boolean`):

Also return a business row for each competitor compared, charged as business rows. Off by default: the benchmark row already lists them with their grade, stars, complaints and reply days.

## `licenseExpiringWithinDays` (type: `integer`):

Return only businesses that have a licence expiring within this many days (1 to 365), or that expired in the last 30 days (flagged). Works with business links and with searches such as "plumber" near "Austin, TX". Businesses without a matching licence are not returned or charged. This returns business rows only, so reviews, complaints and summaries are skipped. Leave empty to turn off.

## `stars` (type: `array`):

Only reviews with these ratings. Leave empty for all. Does not apply to complaints.

## `dateFrom` (type: `string`):

Only reviews and complaints from this date on, like 2026-09-01, or a period back from today like 30 days. Both lists are newest first, so the run stops at this date.

## `withRepliesOnly` (type: `boolean`):

Only reviews the business replied to, and complaints it responded to.

## `onlyNew` (type: `boolean`):

Return only reviews and complaints that earlier runs with the same businesses, searches and filters did not, and business rows only when the grade or counts changed. For daily or weekly schedules. A run with nothing new charges nothing.

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

Stop after this many charged rows (reviews, complaints and businesses) across the whole run. 0 means no limit.

## Actor input object example

```json
{
  "businesses": [
    "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"
  ],
  "searchQueries": [
    "plumber",
    "roofing contractor"
  ],
  "searchLocation": "Austin, TX",
  "searchLocations": [
    "Round Rock, TX",
    "78701"
  ],
  "searchCountry": "USA",
  "maxBusinessesPerSearch": 30,
  "accreditedOnly": false,
  "detailsForFoundBusinesses": false,
  "maxReviewsPerBusiness": 20,
  "maxComplaintsPerBusiness": 10,
  "includeBusinessInfo": true,
  "includeSummary": false,
  "benchmarkCompetitors": false,
  "competitorsToCompare": 10,
  "includeCompetitorRows": false,
  "stars": [
    "1",
    "2"
  ],
  "dateFrom": "2026-09-01",
  "withRepliesOnly": false,
  "onlyNew": false,
  "maxResults": 0
}
```

# Actor output Schema

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

One row per customer review: business, stars, text, date, reviewer display name, the business's reply and any follow-up.

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

One row per complaint: business, type, status, date, the complaint as BBB publishes it, the business's response and the whole exchange.

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

One row per business: BBB grade, accreditation, review stars and count, complaint counts, category, phone, address, how it was found.

## `reputation` (type: `string`):

With Add a reputation summary per business: one row per business with complaint trend, answered and resolved shares, response days, top complaint types, star mix and reply rate, from what the run read.

## `benchmark` (type: `string`):

With Benchmark against local competitors: one row per business with its rank among local competitors, the measures behind it, plain-English highlights and the competitor list.

## `licenseWatch` (type: `string`):

With Licences expiring within: businesses with a trade licence ending soon, the days left and who issued it.

## `full` (type: `string`):

Every field of every row, including free not-found rows.

## `summary` (type: `string`):

Counts, free rows by reason, anything that failed, access stats and cost.

# 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 = {
    "businesses": [
        "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"
    ],
    "maxReviewsPerBusiness": 20,
    "maxComplaintsPerBusiness": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("locaihost/bbb-reviews").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 = {
    "businesses": ["https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"],
    "maxReviewsPerBusiness": 20,
    "maxComplaintsPerBusiness": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("locaihost/bbb-reviews").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 '{
  "businesses": [
    "https://www.bbb.org/us/wa/seattle/profile/ecommerce/amazoncom-1296-7039385"
  ],
  "maxReviewsPerBusiness": 20,
  "maxComplaintsPerBusiness": 10
}' |
apify call locaihost/bbb-reviews --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,locaihost/bbb-reviews"
        }
    }
}
```

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/xVB1hhoDtoLIHADM5/builds/FPUugCyVfspXS63wz/openapi.json
