Goodreads is still where most readers log what they read, so it's where the numbers are: how many people rated a book, how those ratings split across one to five stars, what the most-liked reviews say, and which of an author's books carry their reputation. If you build a reading app, do comp titles for a publisher, or just want to know whether a book you're about to pitch is growing, you'll want that data in a script.
Then you search for "Goodreads API" and find out it's gone. This post covers what's left officially, what goodreads.com hands a plain script today, and then builds goodreads.mjs, one Node.js file with no npm dependencies (built-in fetch and node:sqlite, so Node 22.13+). It has six commands:
-
probechecks what goodreads.com returns to a script and reads the data embedded in a book page -
searchfinds books by title or ISBN -
bookprints details and the full one-to-five-star rating distribution -
reviewspages through community reviews with a cursor and summarizes them -
authorlists an author's books and shows how concentrated their ratings are -
trackstores rating snapshots in SQLite and reports new ratings per day between runs
Every command was run on November 13, 2026 on a rented Linux VM (Node 24.22.1), and each output block below is pasted unedited from that session.
Read this first. Goodreads' Terms of Use forbid this kind of collection. Section 1 ("Use of Our Service") of the terms at goodreads.com/about/terms (last revised April 28, 2021) says your license to use the site does not include "any collection and use of any book listings, descriptions, reviews or other material included in the Service" or "any use of data mining, robots, or similar data gathering and extraction tools." So scraping Goodreads is against its terms, whoever does the fetching. Read the terms yourself and think about the legal risk for your use case before you run anything here, and use the official options below wherever they cover what you need. Reviews are also personal writing by real people: don't republish them, and keep only what you need.
Is there still a Goodreads API?
Not for new projects. In December 2020 Goodreads stopped issuing new developer keys and said it planned to retire the public API. Today goodreads.com/api just redirects to the homepage (you'll see the 302 in Step 1). Old keys you may find in blog posts and GitHub repos don't help a new project.
What you can still use without scraping:
- Your own library export. Under My Books, "Import and export" gives you a CSV of your own shelves, ratings, and review dates. If you only need your own reading data, stop here.
- Open Library. The Internet Archive's Open Library API is open and has ISBN, work, and author lookups. It has ratings too, but far fewer: on November 13 it had about 200 ratings for Project Hail Mary, versus about 1.94 million on Goodreads.
If you need Goodreads' own ratings, distributions, or reviews, there's no official route left. That's what the rest of this post is about.
Step 1: See what goodreads.com sends a script
Book pages are a Next.js app. The HTML contains a <script id="__NEXT_DATA__"> tag with the page's Apollo GraphQL cache, so you don't need to parse markup. The cache holds Book:..., Work:..., Review:..., and User:... entries. The rating statistics live on the work (all editions together), not on the edition you asked for.
#!/usr/bin/env node // goodreads.mjs - Goodreads book, review, and author data in Node.js (22.13+), no npm dependencies. // Usage: node goodreads.mjs <probe|search|book|reviews|author|track> ... import { DatabaseSync } from "node:sqlite"; import { writeFileSync } from "node:fs"; const KEY = process.env.SCRAPEATLAS_API_KEY ?? ""; const API = "https://api.scrapeatlas.com/v1/goodreads"; const UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function get(url, headers = {}) { const r = await fetch(url, { headers, redirect: "manual", signal: AbortSignal.timeout(30_000) }); return { status: r.status, location: r.headers.get("location"), text: await r.text() }; } function nextData(html) { const m = html.match(/<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/s); if (!m) throw new Error("no __NEXT_DATA__ on the page (layout changed or you got a block page)"); return JSON.parse(m[1]).props.pageProps.apolloState; } function bookFromApollo(state) { const values = Object.entries(state); const book = values.find(([k, v]) => k.startsWith("Book:") && v.title)?.[1]; const work = values.find(([k]) => k.startsWith("Work:"))?.[1]; const reviews = values.filter(([k]) => k.startsWith("Review:")).map(([, v]) => v); return { title: book.title, isbn13: book.details?.isbn13, pages: book.details?.numPages, genres: (book.bookGenres ?? []).map((g) => g.genre.name), averageRating: work.stats.averageRating, ratingsCount: work.stats.ratingsCount, ratingsCountDist: work.stats.ratingsCountDist, // [1-star, 2-star, 3-star, 4-star, 5-star] textReviewsCount: work.stats.textReviewsCount, embeddedReviews: reviews.length, }; } async function probe(bookId) { const page = `https://www.goodreads.com/book/show/${bookId}`; let real; for (const ua of ["curl/8.5.0", "", null, UA]) { const res = await get(page, ua === null ? {} : { "user-agent": ua, "accept-language": "en-US,en;q=0.9" }); const label = ua === null ? "Node default UA" : ua === "" ? "empty UA" : ua.slice(0, 15); console.log(`GET /book/show/${bookId} [${label.padEnd(15)}] -> ${res.status}, ${res.text.length} bytes`); if (ua === UA) real = res; } const b = bookFromApollo(nextData(real.text)); console.log(` __NEXT_DATA__: "${b.title}" avg ${b.averageRating}, ${b.ratingsCount} ratings, dist ${JSON.stringify(b.ratingsCountDist)}`); console.log(` genres: ${b.genres.slice(0, 5).join(", ")} | ${b.textReviewsCount} text reviews, ${b.embeddedReviews} embedded in the HTML`); const more = real.text.match(/\/book\/show\/\d+\/reviews\?reviewFilters=[^"]+/); console.log(` "more reviews" link: ${more ? more[0].slice(0, 60) + "..." : "none"}`); const ac = await get(`https://www.goodreads.com/book/auto_complete?format=json&q=${encodeURIComponent(b.title)}`, { "user-agent": UA }); const hits = JSON.parse(ac.text); console.log(`GET /book/auto_complete?format=json -> ${ac.status}, ${hits.length} hits; top: ${hits[0]?.bookId} "${hits[0]?.title}" (${hits[0]?.ratingsCount} ratings)`); const api = await get("https://www.goodreads.com/api", { "user-agent": UA }); console.log(`GET /api -> ${api.status} ${api.location ?? ""}`); } $ node goodreads.mjs probe 54493401 GET /book/show/54493401 [curl/8.5.0 ] -> 200, 744749 bytes GET /book/show/54493401 [empty UA ] -> 403, 919 bytes GET /book/show/54493401 [Node default UA] -> 503, 1356 bytes GET /book/show/54493401 [Mozilla/5.0 (X1] -> 200, 745286 bytes __NEXT_DATA__: "Project Hail Mary" avg 4.51, 1938684 ratings, dist [8979,28675,141888,549311,1209831] genres: Science Fiction, Fiction, Audiobook, Book Club, Fantasy | 265795 text reviews, 30 embedded in the HTML "more reviews" link: /book/show/54493401/reviews?reviewFilters=eyJhZnRlciI6Ik5EUT... GET /book/auto_complete?format=json -> 200, 4 hits; top: 54493401 "Project Hail Mary" (1938672 ratings) GET /api -> 302 https://www.goodreads.com/ What that tells you:
- The data is there. One request gets the average, the rating count, the five-bucket distribution, genres, and the text review count.
- Only 30 reviews come with the page. The "more reviews" link is
/book/show/<id>/reviews?reviewFilters=<base64 cursor>, androbots.txtdisallows both/book/reviews/and/*reviewFilters. It also disallows/search,/work, and/review/list. -
/book/auto_complete?format=jsonis the search box's JSON endpoint. It's handy for title-to-ID lookups, but it only returns a handful of hits. - Status codes are unreliable. An empty User-Agent got
403every time. Everything else changed from run to run: in that probe, the Node default UA got a503("Goodreads - unexpected error") and acurlUA got a200. A few minutes earlier, testing with thecurlcommand itself, thecurlUA got the503, while Node, Pythonrequests, and Go default UAs got200.
To see how bad that last point is, I requested ten book pages in a row, one second apart, for each of three User-Agents. Here are two runs a few minutes apart (the first version of the script only counted status codes):
$ node burst.mjs # run 1 { 'browser 200': 3, 'browser 202': 7, 'empty 403': 10, 'default 202': 10 } $ node burst.mjs # run 2, also checks for __NEXT_DATA__ browser UA {"200 with data":10} empty UA {"403 no data":10} first non-data reply: 403 919B <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML>< Node default {"200 with data":9,"503 no data":1} first non-data reply: 503 1356B <!DOCTYPE html> <html> <head> <title>Goodreads - unexpected error</title> <link href="https://s.gr-assets.com/ So from one cloud IP, you get a mix of 200, 202, and 503 even at one request per second. My advice: check for __NEXT_DATA__ in the body rather than trusting the status code, and expect the Apollo keys to change whenever Goodreads ships a new frontend.
Step 2: A small API client
Reading the book page yourself works for a single book's stats. Paging through reviews means reverse-engineering the reviewFilters cursor that robots.txt asks crawlers to stay away from, and search results and author lists are more page types to parse, each of which can break on its own. So from here on, the script reads Goodreads through ScrapeAtlas*. Its OpenAPI spec lists five Goodreads routes: /v1/goodreads/search, /book, /book-reviews, /author, and /author-books. You call them with GET and pass your key in the x-api-key header. Each successful call, partial results included, costs 1 credit. Failed calls are free. Using an API doesn't change the terms: Goodreads' rules still apply to the data, whoever fetches it.
async function sa(path, params) { if (!KEY) throw new Error("Set SCRAPEATLAS_API_KEY first."); const qs = new URLSearchParams(Object.entries(params).filter(([, v]) => v != null && v !== "")); for (let attempt = 1; ; attempt++) { const r = await fetch(`${API}/${path}?${qs}`, { headers: { "x-api-key": KEY }, signal: AbortSignal.timeout(120_000) }); const body = await r.json().catch(() => ({})); if (r.ok && body.success !== false) return body; const code = body.failure?.code ?? `http_${r.status}`; const retry = r.status === 429 || r.status >= 500; if (!retry || attempt === 3) { const err = new Error(`${path}: ${code} - ${body.failure?.message ?? r.statusText}`); err.code = code; throw err; } await sleep(2000 * attempt); } } // Accepts 54493401, "54493401-project-hail-mary", or a full goodreads.com URL. function idOf(x) { const m = String(x).match(/(?:show\/)?(\d+)/); if (!m) throw new Error(`not a Goodreads id or URL: ${x}`); return m[1]; } sa() retries 429 and 5xx up to three times and throws errors that carry the API's failure.code (for example not_found), so the commands can tell a missing book from a network problem. idOf() takes 54493401, 54493401-project-hail-mary, or a full book or author URL.
Step 3: Search by title or ISBN
async function search(query, page = 1) { const res = await sa("search", { query, page }); console.log(`"${query}": ${res.total ?? "?"} results, page ${res.page}, hasMore=${res.hasMore}`); for (const b of res.books) { const author = b.authors.map((a) => a.name).join(", "); console.log(`${String(b.id).padEnd(10)} ${b.averageRating.toFixed(2)} ${String(b.ratingsCount).padStart(9)} ${String(b.publishedYear ?? "----")} ${b.title.slice(0, 48)} - ${author}`); } } $ node goodreads.mjs search project hail mary "project hail mary": 107 results, page 1, hasMore=true 54493401 4.51 1938672 2021 Project Hail Mary - Andy Weir 119255205 4.45 869 ---- Project Hail Mary / Artemis/ The Martian - Andy Weir 58007522 4.04 25 ---- SUMMARY AND ANALYSIS OF PROJECT HAIL MARY BY AND - Good Reads Publishing 58057170 3.85 20 ---- Summary & Analysis of Andy Weir Book: Project Ha - PressPrint 58663158 3.79 14 ---- SUMMARY OF PROJECT HAIL MARY NOVEL: Detailed Sum - Evergreen Summaries 61157535 3.75 4 ---- Summary: Project Hail Mary: by Andy Weir - Zara Library 60211385 4.67 3 ---- Summary of Project Hail Mary: A Novel by Andy We - rah man 61213069 4.50 4 ---- Study Guide: Project Hail Mary by Andy Weir - SuperSummary 238724014 5.00 3 ---- Project Hail Mary Movie Review: From Page to Pro - Vivian A. Kendrick 58051187 3.60 5 ---- SYNOPSIS OF PROJECT HAIL MARY: An Epic Tale of R - Stephen White 60695472 4.00 1 ---- SUMMARY OF PROJECT HAIL MARY BY ANDY WEIR: A NOV - Harry Thomas 242321051 3.84 220 2026 Mars One: An explosive, race-against-time thrill - Charlotte Robinson 59821976 1.00 1 ---- Summary of Project Hail Mary: A Novel By Andy We - Anis Classics 60556727 2.00 1 ---- Summary Guide of Project Hail Mary by Andy Weir: - Unboxread 59743662 5.00 1 ---- Summary of Project Hail Mary by Andy Weir - Rita Bennet 60748861 3.00 4 ---- Epilogue for Project Hail Mary - Mark C. Tredecim The real book is first. Most of the rest of page 1 is summary books, study guides, and "movie review" booklets: low-effort titles that piggyback on a bestseller's name. If you search by title in bulk, don't just take the first hit blindly. Check that the author matches and that ratingsCount is plausible, or search by ISBN instead:
$ node goodreads.mjs search 9780593135204 "9780593135204": 1 results, page 1, hasMore=false 54493401 4.51 1938684 2021 Project Hail Mary - Andy Weir One exact hit. Two other things to notice. The title search reported 1938672 ratings and the ISBN search 1938684, a few minutes apart, because search results and book pages aren't refreshed at the same time. And publishedYear is null (printed as ----) for many editions and box sets. Here's the same kind of noise for another big book:
$ node goodreads.mjs search sunrise on the reaping "sunrise on the reaping": 52 results, page 1, hasMore=true 214331246 4.50 1401387 2025 Sunrise on the Reaping - Suzanne Collins 217451201 4.66 1104 ---- Hunger Games 5-Book Hardcover Box Set - Suzanne Collins 231118820 4.49 207 ---- The Hunger Games 2 Book Collection Set - Suzanne Collins 231072856 4.44 40 ---- Sunrise on the Reaping: A gripping tale of coura - P. Love 244829102 4.35 20 ---- Sunrise on the Reaping: The Complete Synopsis an - Amy McMahon, Jonah Gathu 252237379 4.67 3 ---- The Hunger Games: Sunrise on the Reaping Movie R - James A. Schultz 238495844 5.00 2 ---- Sunrise on the Reaping: Weaving an unforgettable - Gift Betty 245877500 4.92 3 ---- SUNRISE ON THE REAPING BY SUZANNE COLLINS - Unknown Author 235026481 5.00 1 ---- The Hunger Games: Sunrise on the Reaping Pouch - Unknown Author 244086478 5.00 1 ---- Sunrise on the Reaping: Hunger Game - J Peace 245440175 3.00 1 ---- Sunrise on the Reaping: Hunger Games Book 5 - Unknown Author 231431811 0.00 0 ---- Study Guide: Sunrise on the Reaping by Suzanne C - SuperSummary 231575918 0.00 0 ---- Sinister Sunrise: Gothic Symbols, Rome, and Mode - Valerie Estelle Frankel Step 4: Book details and the rating distribution
The average rating hides most of the story. Two books with a 4.2 can look completely different: one where nearly everyone gave 4 stars, and one that splits between 5 and 1. The distribution shows which one you have.
function ratingStats(dist) { const counts = [1, 2, 3, 4, 5].map((s) => dist[s] ?? 0); const total = counts.reduce((a, b) => a + b, 0); const mean = counts.reduce((a, c, i) => a + c * (i + 1), 0) / total; const share = (stars) => stars.reduce((a, s) => a + counts[s - 1], 0) / total; return { counts, total, mean, five: share([5]), low: share([1, 2]) }; } function bar(n, max, width = 30) { return "#".repeat(Math.round((n / max) * width)); } async function book(ref) { const { book: b } = await sa("book", { id: idOf(ref) }); const st = ratingStats(b.ratingsDistribution); const max = Math.max(...st.counts); console.log(`${b.title} - ${b.authors.map((a) => a.name).join(", ")} (${b.work?.firstPublishedAt?.slice(0, 4) ?? "?"})`); console.log(` ${b.format ?? "?"}, ${b.pages ?? "?"} pages, ISBN13 ${b.isbn13 ?? "-"}, ${b.publisher ?? "-"}`); console.log(` avg ${b.averageRating} from ${b.ratingsCount} ratings (mean of distribution: ${st.mean.toFixed(3)}), ${b.reviewsCount} text reviews`); for (let s = 5; s >= 1; s--) { const n = st.counts[s - 1]; console.log(` ${s}* ${String(n).padStart(9)} ${(100 * n / st.total).toFixed(1).padStart(5)}% ${bar(n, max)}`); } console.log(` 5-star share ${(100 * st.five).toFixed(1)}%, 1-2 star share ${(100 * st.low).toFixed(1)}%`); console.log(` genres: ${b.genres.slice(0, 6).map((g) => g.name).join(", ")}`); const wins = (b.awards ?? []).filter((a) => a.designation === "WINNER"); if (b.awards?.length) console.log(` awards: ${b.awards.length} listed, ${wins.length} wins (${wins.slice(0, 3).map((a) => a.name).join(", ")})`); if (b.series?.length) console.log(` series: ${b.series.map((s) => s.position ? `${s.title} #${s.position}` : `${s.title} (no position given)`).join(", ")}`); return b; } $ node goodreads.mjs book 54493401 https://www.goodreads.com/book/show/214331246-sunrise-on-the-reaping Project Hail Mary - Andy Weir (2021) Hardcover, 476 pages, ISBN13 9780593135204, Ballantine Books avg 4.51 from 1938684 ratings (mean of distribution: 4.507), 265795 text reviews 5* 1209831 62.4% ############################## 4* 549311 28.3% ############## 3* 141888 7.3% #### 2* 28675 1.5% # 1* 8979 0.5% 5-star share 62.4%, 1-2 star share 1.9% genres: Science Fiction, Fiction, Audiobook, Book Club, Fantasy, Space awards: 8 listed, 5 wins (Geffen Award, Audie Award, Nutmeg Book Award) Sunrise on the Reaping - Suzanne Collins (2025) Hardcov...