# Goodreads Books Scraper (`dami_studio/goodreads-books-scraper`) Actor

Search Goodreads by title, author or keyword and get one row per book: title, author, average rating, how many people rated it, year, editions, full-size cover and the book link. No account, no API key, no browser. You only pay for books returned.

- **URL**: https://apify.com/dami\_studio/goodreads-books-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** E-commerce, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.14 / 1,000 book scrapeds

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?

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

## Goodreads Books Scraper

Type a title, an author or a phrase and get the Goodreads books that match, one row each: the title, who wrote it, the average rating, how many people rated it, the year, how many editions exist, the full-size cover image and the Goodreads link. No Goodreads account, no API key, no browser.

- Twenty books come back per page of results, and the run pages down until it has the number you asked for.
- Search several terms in one run. Books that match more than one term are returned once and charged once.
- You can point the search at titles only, authors only, or everything — the same three modes the site itself offers.
- A term that genuinely matches nothing says so in a free diagnostic row. It does not look like a failure, and it does not bill you.
- Cover links point at the full-size image Goodreads holds, not the 75-pixel thumbnail the results page draws.

### Price

**$1.14 per 1,000 books**, plus a **$0.0005 start fee per run**.

This is a flat rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Books | Total cost |
|---|---|
| 100 | $0.1145 |
| 1,000 | $1.1405 |
| 10,000 | $11.4005 |
| 100,000 | $114.0005 |

#### What is actually charged

- **One `book-scraped` event per book row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Books already returned earlier in the same run are dropped before they are charged, so a term that overlaps another one costs you nothing twice.
- A listing Goodreads shows without a rating is skipped rather than charged — you are paying for a rating, so a row without one is not a row.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "queries": [
    "dune",
    "project hail mary"
  ],
  "searchField": "all",
  "maxItems": 40
}
```

| Field | What it does |
|---|---|
| `queries` | Book titles, author names or plain keywords. Up to 20 per run. Each one is searched in turn until the run reaches `maxItems`. |
| `searchField` | Where to look: `all` (the default), `title` or `author`. Searching `author` for "christie" returns her novels; searching `title` returns books with Christie in the name. They are genuinely different result sets. |
| `maxItems` | Total rows across all terms. The budget is split evenly, so four terms and 40 rows gives you ten of each. Default 20, hard ceiling 2,000. Keep it low while you are testing — you pay per row. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per book. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "book",
  "query": "dune",
  "searchField": "all",
  "rank": 1,
  "bookId": "44767458",
  "title": "Dune (Dune, #1)",
  "author": "Frank Herbert",
  "avgRating": 4.29,
  "ratingsCount": 1710264,
  "publishedYear": 1965,
  "editionsCount": 34,
  "url": "https://www.goodreads.com/book/show/44767458",
  "coverUrl": "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1555447414i/44767458.jpg",
  "scrapedAt": "2026-09-20T10:14:02.771Z"
}
```

#### Field notes

- `title` — exactly as Goodreads prints it, series name and number included — "Dune (Dune, #1)". Accents and non-Latin scripts come through intact.
- `author` — the first credited author. A book with a translator or an illustrator listed beside the author returns the author.
- `avgRating` — the Goodreads average at the moment the row was read, 1.00 to 5.00.
- `ratingsCount` — how many people have rated it. This is the number worth sorting on — a 4.8 from nine readers is not a 4.8 from nine hundred thousand.
- `publishedYear` — the year of the edition Goodreads chose to show. **Often null**, and that is the source, not the parse: box sets, study guides and many translated editions are listed with no year at all. On a 219-row run across six terms it was present on 140 of them, and it thins out the deeper you page.
- `editionsCount` — how many editions of the same work Goodreads knows about. A high number is a reliable sign you have found the canonical book rather than a reprint.
- `url` — the clean book page link. Goodreads appends per-request tracking parameters to its own links; those are stripped, so the same book has the same URL on every run.
- `coverUrl` — the full-size cover. The results page serves a 75-pixel thumbnail; the size token is removed so you get the original upload. Nothing is downloaded during the run — this is a link, and it costs you nothing.
- `bookId` — the Goodreads book id. Stable, and safe to use as a primary key across runs.
- `rank` — where the book sat in your results, counting from 1 across the whole run.

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- It reads the public Goodreads search results directly — the same page you would see signed out — and pulls the book fields out of it. No login, no cookies, no headless browser.
- Requests leave through a large pool of rotating addresses. Goodreads throttles by address and answers a throttled request with a short challenge page rather than an error, so the run recognises that page, moves to another address and asks again. Three tries per page, then it gives up on that page and tells you.
- Each term is paged 20 books at a time until your `maxItems` is reached. Book ids already seen in the run are skipped, and when a whole page adds nothing new the term stops there rather than paging into repeats.

### What people use it for

- Sizing up a genre before writing in it — pull 200 books on a theme and sort by `ratingsCount` to see what readers actually turned up for, rather than what a bestseller list says.
- Keeping a reading-app or bookshop catalogue honest: match your ISBNs by title and author, then refresh the rating and the cover link on a schedule.
- Checking an author's back catalogue in one pass — `searchField: "author"` gives you every book credited to them with ratings attached.
- Comp research for a book proposal. Agents ask what your book sits next to; a table of titles, years and rating counts answers it better than a paragraph.
- Filling in cover art and ratings for a list you already have — feed the titles in as queries and take `coverUrl` and `avgRating` back out.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "book"`. One billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when you ran with no search terms, and it is there so you can see the output shape before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NO_RESULTS` when Goodreads has nothing matching a term, `BLOCKED` when the run could not get a search page for a term at all, `ROW_INCOMPLETE` when a listing had no rating to return, `NETWORK` when Goodreads could not be reached, `TIME_BUDGET` when the run ran out of time before reaching a term. Each carries a plain-English `error` and the `query` it belongs to.

`NO_RESULTS` and `BLOCKED` are deliberately separate codes. The first means Goodreads looked and had nothing; the second means the run never got to look. Treating those as the same thing is how a scraper quietly tells you a book does not exist when really it was throttled.

If you only want the data, filter on `charged == true`. That count always equals the number of events you were billed for, so the dataset is its own invoice.

### Limitations

- This does not read book descriptions, ISBNs, page counts, genres, series order, quotes or review text. It reads the search results, and those are the fields the search results carry.
- It does not collect reviewers, their names, their profiles or anything they wrote. Reviews are people's own writing and are out of scope here.
- `publishedYear` is frequently null — Goodreads simply does not print a year on many listings. On a 219-row test run it came back on 140. Every other field was present on all 219.
- Search relevance falls away with depth. The first two or three pages of a title search are the book and its editions; by page twenty you are into loose word matches, and on a short query like "dune" that means titles containing "d'une". Ask for a few hundred rows, not a few thousand, unless you intend to filter afterwards.
- Goodreads never says "that is the last page". Ask for page 900 and it hands back twenty books. The run stops when a page adds no book it has not already seen, which is the only honest end-of-results signal available.
- Ratings and rating counts are a snapshot at read time. They move.
- A term with fewer matches than its share of the budget returns fewer rows. The leftovers are not handed to the other terms.
- Goodreads throttles by address and the run retries three times per page. On a bad minute a page can still be lost; you get a `BLOCKED` row naming the term, not a silently short result.
- Hard ceilings: 2,000 rows and 20 terms per run, 50 pages per term. Split larger jobs across runs.

### Questions

**Do I need a Goodreads account or an API key?**

No. Goodreads stopped issuing API keys years ago — the old developer endpoint answers "Invalid API key" to everyone now. This reads the public search pages instead, signed out, exactly as a visitor would.

**What happens when a search matches nothing?**

You get one uncharged diagnostic row with `errorCode: "NO_RESULTS"` for that term, and the run carries on to your other terms. You are never billed for a term that returned nothing.

**How do I tell "no such book" from "the run got blocked"?**

The `errorCode`. `NO_RESULTS` means Goodreads returned its search page and it was empty. `BLOCKED` means the run could not get the search page at all after three tries from different addresses. They are never conflated, and neither is charged.

**Why is publishedYear empty on some rows?**

Because Goodreads did not print one. Box sets, summaries, study guides and many translated editions are listed without a year. The field is null rather than guessed.

**Can I search by author instead of title?**

Yes — set `searchField` to `author`. The three modes are `all`, `title` and `author`, and they return genuinely different sets. Searching "christie" as an author gives Agatha Christie's novels; as a title it gives books with Christie in the name.

**Will the run fail if Goodreads blocks it?**

No. You get an uncharged diagnostic row explaining what happened and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I run this on a schedule?**

Yes. Nothing is held between runs, so the same input is safe to repeat. Use `bookId` to work out what is new and to spot rating changes.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always false, and the count of charged rows always equals the number of billed events.

# Actor input Schema

## `queries` (type: `array`):

Book titles, author names or plain keywords to search Goodreads for. Up to 20 per run. Each term is searched in turn until the run reaches the row limit below.

## `searchField` (type: `string`):

Which part of a book to match on. 'Everything' is Goodreads' own default and mixes titles, authors and series. 'Title only' and 'Author only' return genuinely different sets - searching 'christie' as an author gives you Agatha Christie's novels, as a title it gives books with Christie in the name.

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

Total number of books to return across all search terms. The budget is shared evenly between the terms, so four terms and 40 rows gives you ten of each. Keep it low while you are testing - you pay per row.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "queries": [
    "dune",
    "project hail mary"
  ],
  "searchField": "all",
  "maxItems": 20
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: query, title, author, avgRating, ratingsCount, publishedYear, url, coverUrl, editionsCount, bookId, searchField, rank. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "queries": [
        "dune",
        "project hail mary"
    ],
    "searchField": "all",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/goodreads-books-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 = {
    "queries": [
        "dune",
        "project hail mary",
    ],
    "searchField": "all",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/goodreads-books-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 '{
  "queries": [
    "dune",
    "project hail mary"
  ],
  "searchField": "all",
  "maxItems": 20
}' |
apify call dami_studio/goodreads-books-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/goodreads-books-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/mwaf8BazTvhQUbuqE/builds/2DLSZM0feMnfNgP1z/openapi.json
