# Trustpilot Scraper: Reviews, Star Breakdown & Company Stats (`abotapi/trustpilot-reviews-scraper`) Actor

Scrape Trustpilot business profiles and reviews on any country domain: TrustScore, the exact 1-5 star distribution in counts and percentages, reply rate, claim and verification status, contact block, plus reviews with verification badge and company reply. Keyword, category sweep or pasted links.

- **URL**: https://apify.com/abotapi/trustpilot-reviews-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 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?

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

## Trustpilot Scraper: Reviews, Star Breakdown & Company Stats

Scrape Trustpilot business profiles and their reviews, on any Trustpilot country domain.

Most Trustpilot scrapers give you a list of review rows. This one also gives you the numbers that
make those reviews *mean* something: the exact 1-5 star distribution, the negative and positive
share, how often the company actually replies and how fast, whether the profile is claimed, and the
full public contact block. Point it at one company, a whole category, or a keyword search.

### What you get

Two row types in one dataset, told apart by `kind`.

#### `kind: "business"` - the company profile

| Field | What it is |
|---|---|
| `identifyingName`, `displayName`, `profileUrl`, `websiteUrl`, `logoUrl` | identity |
| `trustScore`, `stars` | the headline score (0-5) and the rounded star rating |
| `numberOfReviews`, `numberOfReviewsLast12Months` | volume, all time and recent |
| `ratingDistribution` | **exact counts per star**: `{oneStar, twoStar, threeStar, fourStar, fiveStar}` |
| `ratingDistributionPercent` | the same split as percentages |
| `negativeReviewShare`, `positiveReviewShare` | 1-2 star and 4-5 star share of all reviews |
| `replyPercentage`, `averageDaysToReply` | how often, and how fast, the company answers |
| `negativeReviewsWithRepliesCount`, `totalNegativeReviewsCount` | how many complaints got an answer |
| `isClaimed`, `claimedDate`, `isClosed`, `isTemporarilyClosed`, `isCollectingReviews` | profile status |
| `verifiedByGoogle`, `verifiedPaymentMethod`, `verifiedUserIdentity`, `hasCollectedIncentivisedReviews` | trust signals |
| `contactEmail`, `contactPhone`, `contactAddress`, `contactCity`, `contactZipCode`, `countryCode` | the public contact block |
| `primaryCategoryName`, `categories`, `breadcrumbTopLevel/MidLevel/BottomLevel` | classification |
| `reviewLanguages` | how many reviews exist per language |
| `reviewsScraped`, `verifiedReviewsScraped`, `verifiedShareOfScraped`, `repliedShareOfScraped` | stats over the reviews **this run actually read** (a sample, and labelled as one) |

#### `kind: "review"` - one row per review

`reviewId`, `reviewUrl`, `rating`, `title`, `text`, `language`, `likes`, `source`,
`isVerified`, `verificationLevel`, `verificationSource`, `experiencedDate`, `publishedDate`,
`updatedDate`, `hasReply`, `replyText`, `replyPublishedDate`,
`consumerDisplayName`, `consumerCountryCode`, `consumerNumberOfReviews`,
plus all available business enrichment columns, including TrustScore, star distribution,
reply statistics, profile status, categories, public contact details and review-sample statistics.
These fields repeat on every review row so a review export stands on its own. Review IDs,
ratings, text and reviewer fields remain specific to that review. Missing source values stay empty.

Only what the public profile page itself shows is collected. No account, no login, nothing behind a
sign-in wall.

### Modes

**Search mode** finds businesses for you.

- `searchType: "keyword"` plus `query` - Trustpilot's own business search, walked forward.
- `searchType: "category"` plus `category` - sweeps every business in a Trustpilot category
  (the id from the category URL, e.g. `book_store`).

**URL mode** reads exactly what you paste, one entry per line, mixed freely:

```
https://www.trustpilot.com/review/bookshop.org
bookshop.org
https://www.trustpilot.com/categories/book_store
https://www.trustpilot.com/search?query=nike
https://de.trustpilot.com/review/www.example-shop.de
```

A bare company domain works: it is the same thing Trustpilot uses in its own profile URL.

### Review filters (applied by Trustpilot, not by throwing rows away)

Every filter below is pushed into Trustpilot's own review URL, so a narrowed run reads fewer pages
instead of downloading everything and discarding most of it.

| Input | Effect |
|---|---|
| `reviewStars` | keep only 1/2/3/4/5-star reviews (multi-select) |
| `reviewLanguage` | `all` by default - see the note below |
| `reviewSort` | `recency` or `relevance` |
| `verifiedOnly` | only reviews Trustpilot marks verified |
| `reviewsSince` | `last30days` / `last3months` / `last6months` / `last12months` |
| `reviewKeyword` | only reviews whose text matches a keyword, e.g. `refund` |

> **Why `reviewLanguage` defaults to `all`.** Trustpilot itself defaults to the visitor's language
> and hides the rest. On one profile checked while building this actor, the company had 45,986
> reviews but a default English visitor was shown only 30,234 of them. This actor asks for every
> language unless you narrow it, so `numberOfReviews` and the review rows agree.

### Limits and cost controls

- `maxItems` - the run cap, counting business rows and review rows together. Default 50, `0` = no limit.
- `maxReviewsPerBusiness` - a per-business budget (20 reviews per page). Default 20, `0` = every review.
- `maxPages` - upper bound per category / search / review list. Default `0` = as deep as Trustpilot serves.
- `minTrustScore` - skip businesses below a TrustScore.
- `fetchDetails` - Search mode only. Off, businesses ship with just what the listing card carries.

### Resume and recurring updates

- **`resumeFromRunId`** continues ONE interrupted run. Paste the previous run or dataset id; rows it
  already collected are not returned and not billed again.
- **`incrementalMode`** is for a schedule. The first run marks everything `NEW`; later runs return only
  `NEW`, `UPDATED` and `REAPPEARED`, with `changedFields`, `firstSeenAt` and `lastSeenAt`.
  `emitUnchanged` and `emitExpired` add `UNCHANGED` / `EXPIRED` rows (and bill them), both off by default.
  `EXPIRED` is only produced after a run that fully scanned the tracked scope: never when `maxItems`
  capped it, never when a page could not be read, and never on a resumed run.

State is kept separately per search **and** per review-filter setup, so a 5-star campaign never mixes
its baseline with an all-stars one. `stateKey` lets you name a campaign or deliberately share one.

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

Set `mcpConnectors` to pipe every returned row into the tools you already use over Model Context
Protocol. Authorize a connector under Apify → Settings → API & Integrations, then select it here.
Notion gets a page per item (set `notionParentPageUrl`); other connectors get a best-effort write or
digest. `maxNotifyListings` caps how many rows go to each connector. None of this changes the dataset.

### Connection

Trustpilot refuses ordinary connections on every route. The actor tries the connection you configure
in `proxyConfiguration` first and, only once that one has been refused on this run, switches to Apify
Proxy's enhanced connection tier and stays there for the rest of the run, so the wasted first probe is
paid once per run rather than once per page. Keep Apify Proxy enabled. If your plan cannot open that
tier, the run fails with a clear message instead of quietly returning zero rows.

### Pricing

You are charged for:

- **Result** - each row returned, business profile or review.
- **Business profile enrichment** - once per business returned in Search mode whose own profile page
  was read (that is, when `fetchDetails` or `scrapeReviews` is on). Never charged for a pasted profile
  link, never charged for a row incremental mode suppressed.
- **Actor Start** - once per run.

### Sample output (illustrative, not a real business or reviewer)

```json
{
  "kind": "business",
  "recordId": "business:example-shop.test",
  "identifyingName": "example-shop.test",
  "displayName": "Example Shop",
  "profileUrl": "https://www.trustpilot.com/review/example-shop.test",
  "trustScore": 4.3,
  "stars": 4,
  "numberOfReviews": 1000,
  "ratingDistribution": { "oneStar": 100, "twoStar": 50, "threeStar": 50, "fourStar": 200, "fiveStar": 600 },
  "ratingDistributionPercent": { "oneStar": 10.0, "twoStar": 5.0, "threeStar": 5.0, "fourStar": 20.0, "fiveStar": 60.0 },
  "negativeReviewShare": 15.0,
  "positiveReviewShare": 80.0,
  "replyPercentage": 62,
  "averageDaysToReply": 2,
  "isClaimed": true,
  "primaryCategoryName": "Book Store",
  "contactEmail": "support@example-shop.test",
  "contactPhone": "(555) 010-0100",
  "reviewsScraped": 20,
  "verifiedShareOfScraped": 85.0,
  "changeType": "NEW"
}
```

```json
{
  "kind": "review",
  "recordId": "review:00000000000000000000000",
  "reviewId": "00000000000000000000000",
  "reviewUrl": "https://www.trustpilot.com/reviews/00000000000000000000000",
  "businessIdentifyingName": "example-shop.test",
  "rating": 5,
  "title": "Sample review title",
  "text": "Sample review body. This is not a real customer review.",
  "language": "en",
  "isVerified": true,
  "verificationLevel": "verified",
  "experiencedDate": "2026-01-01T00:00:00.000Z",
  "publishedDate": "2026-01-02T00:00:00.000Z",
  "hasReply": true,
  "replyText": "Sample company reply.",
  "consumerDisplayName": "Sample Reviewer",
  "consumerCountryCode": "US",
  "changeType": "NEW"
}
```

### Notes

- Reviews are public content. Reviewer rows carry only what Trustpilot itself publishes on the page:
  a display name, a country, a public review counter and the review text. Nothing is collected that
  needs an account, and nothing is inferred beyond the page.
- `verifiedShareOfScraped` and `repliedShareOfScraped` describe the reviews **this run read**, not the
  whole review base. `ratingDistribution` is exact, straight from Trustpilot.
- Country domains (`uk.`, `de.`, `fr.`, `nl.`, `dk.`, `it.`, `es.`, ...) share one grammar. Set `site`,
  or just paste a link on that domain.

### Local development

```bash
python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt
PATH=.venv/bin:$PATH apify run --purge
.venv/bin/python -m pytest tests/ -q
```

# Actor input Schema

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

Search finds businesses by keyword or sweeps a whole Trustpilot category. URL mode reads the exact Trustpilot links (or bare company domains) you paste.

## `searchType` (type: `string`):

Search mode only. Keyword searches Trustpilot's business index; Category sweeps every business listed under one Trustpilot category id.

## `query` (type: `string`):

Search mode with Search type = Keyword. A company name, brand or domain, for example "bookshop.org" or "nike".

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

Search mode with Search type = Category. The Trustpilot category id from the category page URL, for example "book\_store" from https://www.trustpilot.com/categories/book\_store. Also accepts a full category URL.

## `urls` (type: `array`):

URL mode only. One entry per line. Accepts a Trustpilot profile link (https://www.trustpilot.com/review/bookshop.org), a bare company domain (bookshop.org), a category link (https://www.trustpilot.com/categories/book\_store) or a search link (https://www.trustpilot.com/search?query=nike). Category and search links are walked forward; profile links and domains are read as single businesses. Multi-value supported.

## `site` (type: `string`):

Which Trustpilot country domain to read. Applies in BOTH modes (a pasted link keeps its own domain). Examples: www.trustpilot.com (global), uk.trustpilot.com, de.trustpilot.com, fr.trustpilot.com, nl.trustpilot.com, dk.trustpilot.com, it.trustpilot.com, es.trustpilot.com. Each domain fronts the same business data but ranks and localises its category listings for that market.

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

Return each business's individual reviews as their own rows (reviewer display name, rating, title, text, verification badge, dates, and the company's reply). Turn this off to return business profiles only. 💲 In Search mode, turning this ON (or Fetch business details) reads each discovered business's own profile page and charges the profile-enrichment event once per business returned.

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

How many reviews to return for each business (20 per page). This is a per-business budget, not the run cap: Max items below is what bounds the whole run. Use 0 to take every review Trustpilot serves for the active filters.

## `reviewStars` (type: `array`):

Only return reviews with these star ratings. Leave empty for all ratings. Applied by Trustpilot itself, so the star breakdown on the business row still reports the FULL distribution while the review rows are narrowed.

## `reviewLanguage` (type: `string`):

Two-letter language code to restrict reviews to, for example "en", "de", "it". The default "all" reads every language: Trustpilot itself defaults to the visitor's language and silently hides the rest, which is why a business with 45,986 reviews shows only 30,234 to a default English visitor.

## `reviewSort` (type: `string`):

Order Trustpilot returns reviews in.

## `verifiedOnly` (type: `boolean`):

Only return reviews Trustpilot marks as verified (an invitation or a confirmed purchase behind them). Applied by Trustpilot itself.

## `reviewsSince` (type: `string`):

Only return reviews published within this window. Applied by Trustpilot itself.

## `reviewKeyword` (type: `string`):

Only return reviews whose text matches this keyword, for example "refund" or "delivery". Leave empty for all reviews. Applied by Trustpilot itself.

## `fetchDetails` (type: `boolean`):

Search mode only (a pasted profile link or domain always returns full details). Reads each discovered business's own profile page to add the exact star-distribution breakdown, reply behaviour, claim and verification status, full contact block and category breadcrumb. Turn it off to return only what the listing card carries. 💲 Charges the profile-enrichment event once per business returned, as described in the section caption.

## `minTrustScore` (type: `integer`):

Only return businesses whose TrustScore is at least this value (0 to 5). Leave at 0 for all businesses. Applied after each business is read, in both modes.

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

Hard cap on the total number of rows returned across the whole run, counting business profiles and review rows together. Use 0 for no limit; the run then stops when the results run out.

## `maxPages` (type: `integer`):

Upper bound on how many pages each category, search or business review list is walked through. Defaults wide open (0), so Max items is the only cap that stops a run early.

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

Paste a previous run ID or dataset ID to continue a large pull without returning, or charging for, rows already collected there. Use this after an interrupted run. For recurring 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 every business and review as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED rows. Turn on Emit unchanged or Emit expired only when you also want those rows returned (and billed). State is kept separately for each search and review-filter setup; use State key to name or deliberately share a monitoring campaign.

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

Optional. Name this monitoring campaign to keep its state stable, or deliberately share state across differently configured runs. Leave empty to let the actor derive a key automatically from the search and review-filter settings.

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

Off by default. Turn on to also return rows that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have.

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

Off by default. Turn on to also return rows that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked scope: not when Max items capped it, not when a page could not be read, and not when Resume was used. This returns, and bills, extra synthetic rows.

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

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

Apify Proxy settings. Trustpilot refuses ordinary connections on every route, so this actor tries the connection you configure here first and, only if that one is refused, switches once to Apify Proxy's enhanced connection tier for the rest of the run. Keep Apify Proxy enabled or the run will have nothing to fall back to.

## Actor input object example

```json
{
  "mode": "search",
  "searchType": "keyword",
  "query": "bookshop.org",
  "category": "book_store",
  "urls": [
    "https://www.trustpilot.com/review/bookshop.org"
  ],
  "site": "www.trustpilot.com",
  "scrapeReviews": true,
  "maxReviewsPerBusiness": 20,
  "reviewStars": [],
  "reviewLanguage": "all",
  "reviewSort": "recency",
  "verifiedOnly": false,
  "reviewsSince": "any",
  "reviewKeyword": "",
  "fetchDetails": true,
  "minTrustScore": 0,
  "maxItems": 50,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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",
    "searchType": "keyword",
    "query": "bookshop.org",
    "category": "book_store",
    "urls": [
        "https://www.trustpilot.com/review/bookshop.org"
    ],
    "site": "www.trustpilot.com",
    "scrapeReviews": true,
    "maxReviewsPerBusiness": 20,
    "reviewLanguage": "all",
    "reviewSort": "recency",
    "verifiedOnly": false,
    "reviewsSince": "any",
    "fetchDetails": true,
    "maxItems": 50,
    "maxPages": 0,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "maxNotifyListings": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/trustpilot-reviews-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",
    "searchType": "keyword",
    "query": "bookshop.org",
    "category": "book_store",
    "urls": ["https://www.trustpilot.com/review/bookshop.org"],
    "site": "www.trustpilot.com",
    "scrapeReviews": True,
    "maxReviewsPerBusiness": 20,
    "reviewLanguage": "all",
    "reviewSort": "recency",
    "verifiedOnly": False,
    "reviewsSince": "any",
    "fetchDetails": True,
    "maxItems": 50,
    "maxPages": 0,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "maxNotifyListings": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/trustpilot-reviews-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",
  "searchType": "keyword",
  "query": "bookshop.org",
  "category": "book_store",
  "urls": [
    "https://www.trustpilot.com/review/bookshop.org"
  ],
  "site": "www.trustpilot.com",
  "scrapeReviews": true,
  "maxReviewsPerBusiness": 20,
  "reviewLanguage": "all",
  "reviewSort": "recency",
  "verifiedOnly": false,
  "reviewsSince": "any",
  "fetchDetails": true,
  "maxItems": 50,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/trustpilot-reviews-scraper --silent --output-dataset

```

## MCP server setup

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