BBB API endpoint
Returns a business's BBB complaint history: total complaint count, complaints closed or filed in the last 12 months, and one page of per-complaint detail (date, type, status, narrative text, and any business response / customer answer thread). Complaints are listed newest first, 10 per page; use page with the returned page, total_pages and has_more to walk the full list. A business with no filed complaints returns total_complaints 0 and an empty complaints list -- this is a normal result, not an error.
/bbb/business/complaintsReturns a Better Business Bureau (bbb.org) business's complaint history.
Query parameters:
url (required) — a BBB business profile URL, taken from a bbb-search result's url (or hq_profile_url for a multi-location business). Same URL accepted by bbb-business. A location URL ending in /addressId/<number>, which bbb-search and bbb-category return for one address of a multi-location business, is accepted: BBB publishes this page for the business, not per address, so it is read from the business profile and source_url shows that address.page (optional, default 1) — page of the complaint list, 1 or greater. BBB lists complaints newest first, 10 per page. A value below 1 or a non-numeric value returns a 400.Response includes total_complaints (the filed-complaint count for BBB's reporting window, typically 3 years), period_text, and one 12-month count: closed_last_12_months or filed_last_12_months, whichever BBB shows for that business (the other is omitted). The totals are returned on every page. A business with no filed complaints returns total_complaints: 0 and an empty complaints array — this is a normal, common result, not an error. complaints is always an array, never null.
Pagination: each response carries page (the page returned), total_pages (the last page of BBB's list) and has_more. To retrieve every complaint, start at page=1 and request the next page while has_more is true. Each page is a separate billed request. BBB publishes the details of fewer complaints than total_complaints counts, so total_pages × 10 can be lower than total_complaints. A page past the end returns an empty complaints array with has_more: false and no total_pages.
Profile URLs from bbb-search and bbb-category are the ones BBB's listings link to. For some businesses that is a shorter slug than BBB's canonical one, and BBB redirects it to the canonical profile without the ?page= part, so a page above 1 would arrive as page 1. When that happens the endpoint requests the page again at the canonical URL the page names and returns that; source_url then shows the canonical URL. If BBB still answers with a different page than the one requested, the response is an error, never the wrong page.
published_complaints is how many complaints BBB publishes details for, the ones the pages list; hidden_complaints is how many it counts in total_complaints without publishing them (for example 3,778 and 408 against a total of 4,186). Both are returned on every page when the page carries them and are left out otherwise, so 0 is a real zero.
Each entry in complaints[] carries date, type (e.g. Service or Repair Issues), status, text (the complainant's narrative — BBB itself redacts personal information inline as REMOVED), and optional business_responses[] / customer_answers[] threads (each entry has date and text; a dispute can go through several rounds). status is one of BBB's fixed values: Resolved, Unresolved, Answered, Unanswered, Unpursuable.
complaints_list_incomplete: true means BBB counts complaints for the business but the itemized list is missing: total_complaints is above 0 and complaints is empty on the first page. It was first observed live on a high-volume multi-location business, where the page shows its own "no matching results, remove some applied search filters" message with no filters applied. Treat the totals as authoritative and complaints as unavailable, not zero, when this flag is set. An empty first page therefore reads one of two ways: total_complaints: 0 (no complaints filed) or complaints_list_incomplete: true (list unavailable). An empty page above 1 with has_more: false and no total_pages is a page past the end of the list and does not set the flag.
profile_under_review: true means BBB is updating the business profile and has taken the complaint summary and list off the page while it does. notice carries BBB's own sentence ("This profile is currently being updated. You will not be able to read any existing complaints at this time, but you may file a complaint."). complaints is empty and complaints_list_incomplete is true on every page. total_complaints and closed_last_12_months are the counts BBB still embeds in the page and are 0 when it embeds none, so read them as BBB's last published figures, not as a current list. This is BBB's state for the profile: the same request returns the same answer until the review ends, so retrying does not help.
| Field | In the sample | Value in the sample |
|---|---|---|
| business_name | Has a value | “Calixto Plumbing” |
| total_complaints | Has a value | 0 |
| submit_complaint_url | Has a value | bbb.org |
| page | Has a value | |
| has_more | Has a value | false |
| published_complaints | Has a value | 0 |
| hidden_complaints | Has a value | 0 |
| complaints | Empty |
| Parameter | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
| url | string | Yes | BBB business profile URL, from a bbb-search result's url or hq_profile_url. A location URL ending in /addressId/<number> is accepted and read from the business profile | https://www.bbb.org/us/tx/austin/profile/plumber/calixto-plumbing-0825-1000223803 | |
| page | integer | No | 1 | Complaint list page, 1 or greater (10 complaints per page, newest first). Defaults to 1 Minimum: 1. | 1 |
| x-api-key (header) | string | Yes | API key required |
curl -X GET "https://api.crawlora.net/api/v1/bbb/business/complaints?url=https%3A%2F%2Fwww.bbb.org%2Fus%2Ftx%2Faustin%2Fprofile%2Fplumber%2Fcalixto-plumbing-0825-1000223803&page=1" \ -H "x-api-key: $CRAWLORA_API_KEY"
{
"code": 200,
"msg": "OK",
"data": {
"business_name": "Abacus Plumbing, Air Conditioning & Electrical",
"total_complaints": 8,
"period_text": "in the last 3 years.",
"closed_last_12_months": 2,
"submit_complaint_url": "https://www.bbb.org/file-a-complaint/0825-1000193414",
"page": 1,
"total_pages": 1,
"has_more": false,
"complaints": [
{
"id": "0825_1000193414_24518957",
"date": "02/12/2026",
"type": "Service or Repair Issues",
"status": "Answered",
"text": "Abacus installed a Navien tankless water heater at my house...",
"business_responses": [
{
"date": "04/08/2026",
"text": "Dear Mr. REMOVED you for the opportunity to respond to your concerns..."
}
]
}
],
"source_url": "https://www.bbb.org/us/tx/austin/profile/plumbing-and-heating/abacus-plumbing-air-conditioning-electrical-0825-1000193414/complaints"
}
}Request schema
No body schema
Response schema
#/definitions/bbb.complaintsResponseDoc
| Field | Type | Required | Enum | Bounds | Example | Description |
|---|---|---|---|---|---|---|
| code | integer | No | 200 | |||
| data | bbb.ComplaintsResponse | No | ||||
| data.business_name | string | No | ||||
| data.closed_last_12_months | integer | No | 2 | ClosedLast12Months and FiledLast12Months are the two wordings BBB uses for the second summary line ("N complaints closed|filed in the last 12 months."). A page shows one of them; the other field is omitted. | ||
| data.complaints | array | No | ||||
| data.complaints[].business_responses | array | No | ||||
| data.complaints[].business_responses[].date | string | No | ||||
| data.complaints[].business_responses[].text | string | No | ||||
| data.complaints[].customer_answers | array | No | ||||
| data.complaints[].customer_answers[].date | string | No | ||||
| data.complaints[].customer_answers[].text | string | No | ||||
| data.complaints[].date | string | No | ||||
| data.complaints[].id | string | No | ||||
| data.complaints[].status | string | No | Resolved | Status is one of BBB's fixed complaint-status values: Resolved, Unresolved, Answered, Unanswered, Unpursuable. Passed through as rendered rather than validated, since it is an upstream-controlled response value, not a request parameter. | ||
| data.complaints[].text | string | No | ||||
| data.complaints[].type | string | No | Service or Repair Issues | |||
| data.complaints_list_incomplete | boolean | No | ComplaintsListIncomplete is true when TotalComplaints is non-zero but BBB's itemized complaint list came back empty anyway. Confirmed live on at least one high-volume multi-location business (770 total complaints, page rendered "There are no matching results. Improve your results by removing some applied search filters." with no filters actually applied by this client) -- an upstream filter/ pagination quirk, not a parser failure. It is also set for any first page that counts complaints but lists none, whatever BBB renders in place of the list. TotalComplaints/ClosedLast12Months remain authoritative; Complaints will be empty even though real records exist upstream. A page past the end of the list is not incomplete. | |||
| data.filed_last_12_months | integer | No | 3 | |||
| data.has_more | boolean | No | false | |||
| data.hidden_complaints | integer | No | 408 | |||
| data.notice | string | No | This profile is currently being updated. You will not be able to read any existing complaints at this time, but you may file a complaint. | |||
| data.page | integer | No | 1 | Page is the page of the complaint list this response holds. TotalPages is the last page BBB's own pager links to; it is omitted when BBB renders no list (no complaints, or a page past the end). BBB lists fewer complaints than TotalComplaints counts, so TotalPages can be smaller than TotalComplaints/10. HasMore is true while Page is before TotalPages. | ||
| data.period_text | string | No | in the last 3 years. | |||
| data.profile_under_review | boolean | No | ProfileUnderReview is true when BBB is updating the profile and has taken the complaint summary and list off the page for the duration. Notice is BBB's own sentence saying so. TotalComplaints and ClosedLast12Months then come from the counts BBB still embeds in the page and stay zero when it embeds none; Complaints is empty and ComplaintsListIncomplete is true. Seen live 2026-10-12; the state is BBB's, so asking again returns the same answer until the review ends. | |||
| data.published_complaints | integer | No | 3778 | PublishedComplaints is how many complaints BBB publishes details for, the ones the pages list. HiddenComplaints is how many it counts in TotalComplaints without publishing them. Both come from the page's embedded state and are left out when it does not carry them. | ||
| data.source_url | string | No | ||||
| data.submit_complaint_url | string | No | ||||
| data.total_complaints | integer | No | 8 | |||
| data.total_pages | integer | No | 1 | |||
| msg | string | No | OK |
| Status | Description | Schema |
|---|---|---|
| 400 | Bad Request | #/definitions/app.Response |
| 404 | Not Found | #/definitions/app.Response |
| 500 | Internal Server Error | #/definitions/app.Response |
| 503 | Service Unavailable | #/definitions/app.Response |
Use environment variables for secrets and keep Crawlora API keys server-side.
curl -X GET "https://api.crawlora.net/api/v1/bbb/business/complaints?url=https%3A%2F%2Fwww.bbb.org%2Fus%2Ftx%2Faustin%2Fprofile%2Fplumber%2Fcalixto-plumbing-0825-1000223803&page=1" \
-H "x-api-key: $CRAWLORA_API_KEY"