GuidesGOODREADS
Goodreads API without a key
Goodreads stopped issuing API keys on December 8, 2020, but every public shelf is still an RSS feed you can read from the browser: prefix https://proxy.cors.dev/ to https://www.goodreads.com/review/list_rss/USER_ID?shelf=read and parse the XML with DOMParser. It's free, needs no signup or key, returns 100 books per request and allows 600 requests per minute per client IP. Goodreads sends no Access-Control-Allow-Origin header, so the same fetch() without the proxy is blocked by CORS. For your own library, the CSV export is quicker.
What is left of the Goodreads API
The notice is still on the Goodreads developer pages: as of December 8, 2020, no new developer keys, and plans to retire the API. Keys that went 30 days without use were deactivated. Today /api redirects to the homepage and legacy XML endpoints such as /search/index.xml answer 401. Three kinds of public URL still work without a key, and none of them sends Access-Control-Allow-Origin.
| URL | Returns | Size |
|---|---|---|
| /review/list_rss/1?shelf=read | RSS, 100 books with ratings and reviews | 582 KB |
| /review/list_rss/1?shelf=currently-reading | RSS, 9 books | 41 KB |
| /review/list_rss/1?shelf=to-read&per_page=200 | RSS, 200 books | 821 KB |
| /review/list_rss/1?shelf=read&per_page=200 | 502 response_too_large (1,330 KB upstream) | - |
| /user/updates_rss/1 | RSS, 10 latest updates | 26 KB |
| /book/auto_complete?format=json&q=dune | JSON, 5 books | 5.3 KB |
| /search/index.xml?q=dune | 401, the legacy API needs a key | - |
Read a shelf with fetch
A shelf feed lives at https://www.goodreads.com/review/list_rss/USER_ID?shelf=SHELF. Use the shelf slug from the profile: read, currently-reading, to-read or any custom shelf; shelf=%23ALL%23, or no shelf parameter, mixes every shelf. Prefix the URL with https://proxy.cors.dev/ and parse the text as XML.
async function fetchShelf(userId, shelf = 'read', page = 1) {
const feedUrl = `https://www.goodreads.com/review/list_rss/${userId}?shelf=${shelf}&per_page=100&page=${page}`;
const response = await fetch(`https://proxy.cors.dev/${feedUrl}`, { credentials: 'omit' });
if (response.status === 401) throw new Error('This Goodreads profile is private');
if (!response.ok) {
throw new Error(`Shelf request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const feed = new DOMParser().parseFromString(await response.text(), 'application/xml');
if (feed.querySelector('parsererror')) throw new Error('The feed is not valid XML');
return [...feed.querySelectorAll('item')].map((item) => {
const text = (tag) => item.querySelector(tag)?.textContent.trim() ?? '';
return {
bookId: text('book_id'),
title: text('title'),
author: text('author_name'),
rating: Number(text('user_rating')),
readAt: text('user_read_at') ? new Date(text('user_read_at')) : null,
cover: text('book_large_image_url'),
shelves: text('user_shelves').split(', ').filter(Boolean),
link: text('link'),
};
});
}
const books = await fetchShelf('1', 'read');
console.log(books.length, books[0]);user_read_at and user_date_added are RFC 822 dates such as Tue, 18 Aug 2026 00:00:00 +0000, which new Date() parses. Render titles and reviews with textContent; the description element holds HTML.
Fields in each shelf item
| Element | Contains |
|---|---|
| book_id | Goodreads book ID, as in /book/show/ID |
| title, author_name | Book title and first author |
| user_rating | Stars from 1 to 5; 0 when unrated (14 of 100 books) |
| user_read_at | Date finished; empty when not set (17 of 100) |
| user_date_added | Date the book was shelved |
| user_shelves | Custom shelves, comma separated; empty when none |
| user_review | Review text; empty when none (48 of 100) |
| average_rating | Community average, such as 4.38 |
| book_published | Publication year; sometimes empty |
| isbn | ISBN-10; empty on 33 of 100 |
| book_small_image_url to book_large_image_url | Cover image URLs in 4 sizes |
| book > num_pages | Page count |
Big shelves and the 1 MiB limit
Feeds return 100 books by default. per_page goes up to 200; larger values silently fall back to 100. page=2, page=3 and so on walk the rest, and a page past the end is an empty feed of 1.1 KB. Reviews make items heavy: user 1's read shelf averages about 5.8 KB per book, so 200 books come to 1,330 KB, and the free tier, capped at 1 MiB, answers 502 with X-Cors-Error: response_too_large.
| Query | Books | Size | Free tier |
|---|---|---|---|
| shelf=read&per_page=50 | 50 | 303 KB | 200 |
| shelf=read (default) | 100 | 582 KB | 200 |
| shelf=read&per_page=100&page=2 | 100 | 781 KB | 200 |
| shelf=read&per_page=150 | 150 | 986 KB | 200 |
| shelf=read&per_page=200 | 200 | 1,330 KB | 502 response_too_large |
| shelf=to-read&per_page=200 | 200 | 821 KB | 200 |
| shelf=read&per_page=100&page=99 | 0 | 1.1 KB | 200, empty feed |
async function fetchWholeShelf(userId, shelf) {
const books = [];
for (let page = 1; page <= 100; page++) {
const batch = await fetchShelf(userId, shelf, page);
books.push(...batch);
// A page with fewer than 100 books is the last one
if (batch.length < 100) break;
}
return books;
}
const read = await fetchWholeShelf('1', 'read');
console.log(`${read.length} books, ${read.filter((book) => book.rating === 5).length} rated 5 stars`);That loop read all 629 books on user 1's read shelf in 7 requests. If a page of long reviews still crosses 1 MiB, drop to per_page=50; Pro raises the cap to 6 MiB.
Find a user ID
- Open the profile
Open the member profile or any of their shelves on goodreads.com.
- Copy the number
The ID is the number in the URL: goodreads.com/user/show/1-otis-chandler is user 1. Shelf pages use the same number after /review/list/.
- Check that the profile is public
A private profile answers 401 with "Sorry, that person's shelf is private", through the proxy too. An unknown ID answers 404.
A private shelf stays private: the free tier forwards only Accept, If-None-Match and If-Modified-Since upstream, never cookies.
Goodreads export: your library as CSV
For your own books the export beats any feed: one file, every shelf, no paging. It only runs on the desktop site.
- Open My Books
Sign in on goodreads.com in a desktop browser and click My Books.
- Open Import and export
In the left column, under Tools, click Import and export.
- Export Library
Click Export Library. When the file is ready, a link "Your export from [date] - [time]" appears below the button and downloads goodreads_library_export.csv.
Each row is one book, with columns such as Book Id, Title, Author, ISBN13, My Rating, Average Rating, Number of Pages, Original Publication Year, Date Read, Date Added, Bookshelves, Exclusive Shelf, My Review, Private Notes and Read Count. The file never leaves your machine, so reading it in the browser needs no proxy.
Search books by title
The search box on goodreads.com calls https://www.goodreads.com/book/auto_complete?format=json&q=QUERY. It returns a JSON array of up to 5 books with bookId, bookTitleBare, author.name, avgRating (a string), ratingsCount, numPages, imageUrl and bookUrl; the search for dune was 5.3 KB. There is no paging, and a query of just the returns an empty array, so use it for type-ahead, not for crawling the catalog.
const query = 'project hail mary';
const searchUrl = `https://www.goodreads.com/book/auto_complete?format=json&q=${encodeURIComponent(query)}`;
const response = await fetch(`https://proxy.cors.dev/${searchUrl}`, { credentials: 'omit' });
if (!response.ok) {
throw new Error(`Search failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const results = (await response.json()).map((book) => ({
bookId: book.bookId,
title: book.bookTitleBare,
author: book.author.name,
rating: Number(book.avgRating),
ratings: book.ratingsCount,
pages: book.numPages,
cover: book.imageUrl,
url: `https://www.goodreads.com${book.bookUrl}`,
}));
console.log(results);Open Library, Hardcover and Google Books
- Open Library:
https://openlibrary.org/search.json?q=dune&limit=5answered withAccess-Control-Allow-Origin: *in 6.5 KB, so call it directly, no proxy. Results carrytitle,author_name,first_publish_year,isbnandcover_i; covers load fromcovers.openlibrary.org/b/id/COVER_I-M.jpg. - Hardcover: a GraphQL API at
api.hardcover.app/v1/graphqlwith open CORS, but every query needs a token in theAuthorizationheader and Hardcover's docs say queries must not run in the browser. Keep the token in a server or Worker, never in page code. - Google Books: the volumes API allows browser calls, but keyless requests share one daily quota. On our test day every keyless call returned 429 Quota exceeded, direct and through the proxy.
Free tier limits for Goodreads
- Size: 1 MiB per decoded response; 100 books with reviews came to 582 to 781 KB.
- Rate: 600 requests per minute per client IP, with no daily or monthly quota.
- Methods: GET and HEAD, which is all these feeds need.
- Errors: proxy failures carry an
X-Cors-Errorheader; Goodreads 401 and 404 responses pass through unchanged.
Good questions.
Does Goodreads still have an API?
Not for new developers. Goodreads stopped issuing API keys on December 8, 2020 and says it plans to retire the API; goodreads.com/api now redirects to the homepage. Public shelf RSS feeds, update feeds and the search autocomplete still work without a key.
How do I get a Goodreads API key?
You cannot get a new one. Goodreads has issued none since December 2020, and keys unused for 30 days were deactivated. Read public shelves from RSS, or export your own library to CSV.
How do I export my Goodreads library?
On the desktop site open My Books, click Import and export under Tools, then Export Library. Goodreads builds a CSV with one row per book, including your rating, date read, shelves and review.
Why does fetch() to goodreads.com fail with a CORS error?
Goodreads sends no Access-Control-Allow-Origin header on its feeds or autocomplete, so the browser blocks the response. Prefix the URL with https://proxy.cors.dev/ and the response comes back with the header added; free, no signup, 600 requests per minute per client IP.
How many books does a Goodreads RSS feed return?
100 by default. per_page accepts up to 200; larger values fall back to 100. Add page=2, page=3 and so on until a page has fewer books. On the free tier keep 100 per page, because 200 reviewed books can pass the 1 MiB response cap.
Can I read a private Goodreads shelf?
No. A private profile answers 401, "Sorry, that person's shelf is private", to every anonymous request, including through the proxy. The owner can export the library to CSV instead.
What is the best free alternative to the Goodreads API?
Open Library. Its search.json endpoint needs no key and sends Access-Control-Allow-Origin: *, so it works straight from the browser. Hardcover has a richer GraphQL API, but its token belongs on a server.