# Goodreads Book Review Scraper (`superslowsloth/goodreads-reviews-scraper`) Actor

Scrape reader reviews from Goodreads - full review text, star rating, reviewer, likes, comment count and the reader's own shelves, with the book's own figures on every row.

- **URL**: https://apify.com/superslowsloth/goodreads-reviews-scraper.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** Business, Lead generation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.63 / 1,000 book reviews

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Goodreads Book Review Scraper

Returns reader reviews from [Goodreads](https://www.goodreads.com): the full
review text, the star rating, who wrote it, how many readers found it helpful,
how many comments the thread carries, and the reader's own shelf tags. Every
row also carries the book's own figures - title, author, ISBN, page count and
Goodreads' aggregate rating - so a dataset of reviews across several books is
usable without a second lookup.

Give it a book URL, a bare numeric id, or `id-slug`. All three work.

### The full review text, not the truncated one

Goodreads renders a long review twice on the page: a visible copy cut off after
a few hundred characters with a "...more" link, and a hidden copy holding all of
it. This actor reads the hidden one. On the page measured, 22 of 30 reviews were
truncated in the visible copy, so a scraper reading the obvious element would
have silently delivered three quarters of its rows with the ends missing.

### 300 reviews per book is Goodreads' ceiling, not this actor's

Measured 2026-09-11: Goodreads paginates ten pages of thirty reviews and stops,
whatever the book's review count says. A book showing 1,495 reviews serves 300
of them. Page 11 comes back empty rather than failing, and this actor stops
there rather than paying for pages that repeat nothing.

If you need more than 300 rows, ask for more books - not a larger `maxReviews`.

### Output fields

| Field | Notes |
|---|---|
| `id` | Goodreads' review id. |
| `review_url` | Canonical review URL, e.g. `https://www.goodreads.com/review/show/1221838816`. |
| `text` | The full review text, entities decoded and paragraphing kept. |
| `rating` | 1-5, or `null` when the reader did not rate the book. |
| `rating_label` | The phrase Goodreads prints, e.g. `really liked it`. |
| `shelf_status` | What the reader did instead of rating, e.g. `is currently reading it`. |
| `shelves` | The reader's own shelf tags, e.g. `["dropped", "programming"]`. Often empty. |
| `reviewed_at` | The date as printed, e.g. `Mar 08, 2015`. |
| `likes` | How many readers marked the review helpful. |
| `comment_count` | Comments on the thread. `null` when Goodreads shows no comment control at all. |
| `of_another_edition` | Whether the review is of a different edition of the same work. |
| `user_id`, `user_name`, `user_url`, `user_image_url` | The reviewer. |
| `book_id`, `book_url`, `book_title` | The book, repeated on every row. |
| `book_isbn`, `book_format`, `book_language`, `book_page_count`, `book_image_url` | The edition. |
| `book_average_rating`, `book_rating_count`, `book_review_count` | Goodreads' own aggregate figures. |
| `book_authors` | The book's authors. |

### What this source does not give

Stated rather than guessed. A missing value is always `null`, never `0` and
never an empty string, because a zero reads as a measurement that was never
taken.

- **A review can have no rating.** A reader who shelves a book without rating
  it still writes a review, and Goodreads' own API reports that as `0`. It is
  emitted here as `null`, because a `0` in a rating column reads as one star to
  anything that averages it. Those reviews are excluded from any minimum-rating
  filter for the same reason.
- **`comment_count` is `null`, not `0`, when there is no comment control.**
  No control at all is not the same as a thread with nothing in it.
- **No review timestamp, only a date.** Goodreads prints `Mar 08, 2015` with no
  time and no timezone, so no ISO timestamp is invented from it.
- **No spoiler flag, no "read dates", no edition of the reader's copy** beyond
  the `of_another_edition` marker. The legacy review markup does not carry them.
- **`book_review_count` is Goodreads' own published figure** and does not agree
  exactly with the number of reviews it will serve you. For the book measured
  it said 1,500 while the site's own connection reported 1,495 and it served
  300\.

### How the reviews are actually fetched

Worth knowing, because it explains the field list. Goodreads' modern book page
carries the first thirty reviews and **ignores `?page=2`** - the second page
comes back with the same thirty review ids and the identical page token, so a
scraper paging that way delivers thirty duplicates per page and charges for
every one. Real pagination there needs a GraphQL key dug out of the page and a
query id dug out of a JavaScript bundle.

This actor uses Goodreads' older review endpoint instead, which paginates
properly, needs no key, and returns server-rendered markup. Its pages do not
overlap - pages 1, 4 and 10 of the same book shared no review ids at all.

### Anti-bot behaviour

Goodreads answered a plain datacenter address with no proxy on 2026-09-11, so a
proxy is optional. One trap is handled explicitly: **a book that does not exist
comes back with HTTP 200**, carrying Goodreads' own not-found page. The status
line cannot tell it from a real book. This actor tells them apart by the
schema.org block - a real book page has one, the not-found page does not, and a
refusal has neither the block nor the site's own payload. A missing book is
reported and never retried; a refusal is retried on a fresh exit address.

### Input

```json
{
  "startUrls": [
    { "url": "https://www.goodreads.com/book/show/3735293-clean-code" }
  ],
  "maxReviews": 100,
  "minRating": "any",
  "proxyConfiguration": { "useApifyProxy": true }
}
```

# Actor input Schema

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

Goodreads books to scrape reviews from. A full book URL, a bare numeric id, or `id-slug` are all accepted - e.g. https://www.goodreads.com/book/show/3735293-clean-code, or just 3735293.

## `maxReviews` (type: `integer`):

Budget for the whole run, across every book, not per book. Goodreads itself serves at most 300 reviews for any one book however many it says the book has, so ask for more books rather than a bigger number if you need more than that.

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

Only return reviews at or above this rating. Reviews with no rating at all - where the reader shelved the book without rating it - are excluded by any setting other than Any, because there is no rating to compare and treating its absence as a pass or as a zero would both be inventions.

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

Optional. Goodreads answered a plain datacenter address with no proxy when this actor was built on 2026-09-11. A proxy gives the scraper a fresh exit address to retry on if the site starts refusing requests from the platform's own addresses.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.goodreads.com/book/show/3735293-clean-code"
    }
  ],
  "maxReviews": 100,
  "minRating": "any",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `reviews` (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 = {
    "startUrls": [
        {
            "url": "https://www.goodreads.com/book/show/3735293-clean-code"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/goodreads-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 = {
    "startUrls": [{ "url": "https://www.goodreads.com/book/show/3735293-clean-code" }],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/goodreads-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 '{
  "startUrls": [
    {
      "url": "https://www.goodreads.com/book/show/3735293-clean-code"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/goodreads-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/goodreads-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/4vFydeYRaC2eGn44p/builds/bALGIBYaVGQAmh9YG/openapi.json
