# Open Library Scraper: Books, Authors & Editions (`arman-bd/openlibrary-books-scraper`) Actor

Scrape Open Library by search term, ISBN or subject: title, authors, ISBNs, publishers, first publication year, subjects, cover images and edition lists. No key.

- **URL**: https://apify.com/arman-bd/openlibrary-books-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Automation, AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.37 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## Open Library Scraper: Books, Authors & Editions

![Open Library Scraper: Book records by search, ISBN or subject, authors, every ISBN, publishers, first publish year and editions](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/openlibrary-books-scraper.jpg)

Turns a search term, an ISBN or a subject heading into structured book records: title, authors, every ISBN across every printing, publishers, first publication year, subjects, languages, cover image and edition list.

Open Library is the Internet Archive's open bibliographic database. This Actor reads it directly, so there is no browser, no proxy, no credentials and no account.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openlibrary-books-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openlibrary-books-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `query`, `queryType` | Which input produced this record, and whether it came from a search, an ISBN or a subject |
| `olKey`, `url` | Open Library work key (`/works/OL893516W`) and the human-readable page |
| `title`, `subtitle` | Work title and subtitle |
| `authors`, `authorKeys` | Author names and their Open Library author keys |
| `isbn10`, `isbn13` | Every ISBN Open Library knows for the work, split by length |
| `publishers` | Every publisher across all editions |
| `firstPublishYear` | Year of the earliest known edition |
| `numberOfPages` | Median page count across editions |
| `subjects` | Subject headings, capped at 50 |
| `languages` | 3-letter MARC language codes across editions |
| `coverUrl` | Large cover image URL, or `null` when no cover is on file |
| `editionCount` | How many printings Open Library has catalogued |
| `ratingsAverage`, `ratingsCount` | Community rating, `null` when nobody has rated it |
| `editions` | Per-printing detail when `includeEditions` is on. `null` when you did not ask for editions, and also when the edition lookup itself failed. Never a fabricated empty list |
| `scrapedAt` | Run timestamp |

`RUN_SUMMARY` in the key-value store holds the counts and, more usefully, everything that did not happen: `failures` (queries that errored), `targetsWithoutMatches` (queries that ran and matched nothing), `targetsSkipped` (queries never run because `maxResults` was already spent), `rejectedInputs` (entries that could not be turned into a query at all) and `editionFailures` (books delivered with `editions: null` because that lookup failed). A missing book is always traceable to one of those lines.

### Use cases

- **Catalogue enrichment.** Turn a spreadsheet of ISBNs into full records with covers and subjects.
- **Reading-list and library apps.** Search by title or author and store the result as-is.
- **Publishing market research.** Count editions and publishers per subject or per year.
- **AI agents and RAG.** A permissively licensed book-metadata layer with no key to rotate.
- **Deduplication.** Resolve any ISBN to its parent work so different printings collapse to one row.

### Quick start

One search, twenty books:

```json
{
 "searchQueries": ["dune"]
}
```

ISBNs from a catalogue, with every printing attached:

```json
{
 "isbns": ["9780441013593", "0-441-01359-0"],
 "includeEditions": true,
 "maxEditionsPerBook": 25
}
```

Subject sweep for market research:

```json
{
 "subjects": ["science fiction", "cryptography"],
 "searchQueries": ["author:le guin"],
 "maxResults": 200
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchQueries` | array | `[]` | Free-text searches. Open Library's field syntax works too: `author:le guin`, `title:dune`, `publisher:penguin`. |
| `isbns` | array | `[]` | ISBN-10 or ISBN-13. Hyphens and spaces are stripped. Resolves to the parent work, so you get all printings, not just the one you asked for. |
| `subjects` | array | `[]` | Subject headings. Multi-word values are quoted automatically. |
| `includeEditions` | boolean | `false` | Attach per-printing detail. Costs one extra request per book. |
| `maxEditionsPerBook` | integer | `10` | Cap on attached editions, 1 to 100. Only read when `includeEditions` is on. It bounds the edition list inside a row, not the number of rows. |
| `maxResults` | integer | `20` | Hard ceiling on the books saved by the **whole run**, 1 to 1000, across every query, ISBN and subject together. Three queries at `maxResults: 20` save 20 books in total, not 60. |

At least one of `searchQueries`, `isbns` or `subjects` must be non-empty. All three can be used together: results are merged and de-duplicated on `olKey`, so a book found by two different queries is saved once.

`maxResults` is shared evenly between your targets and whatever one target leaves unused passes to the ones after it, so three queries at `maxResults: 30` take 10 each, and if the first matches only 2 books the remaining 28 are available to the other two. Values outside 1 to 1000 are rejected before anything is fetched, rather than quietly rounded into a different run than the one you asked for.

### Output example

```json
{
 "query": "dune",
 "queryType": "search",
 "olKey": "/works/OL893516W",
 "url": "https://openlibrary.org/works/OL893516W",
 "title": "Children of Dune",
 "subtitle": null,
 "authors": ["Frank Herbert"],
 "authorKeys": ["OL79034A"],
 "isbn10": ["0425071790", "0441015905", "0575074906"],
 "isbn13": ["9782266027229", "9780441104024", "9780425079027"],
 "publishers": ["Orion Publishing Group, Limited", "Pocket", "Berkley Publishing"],
 "firstPublishYear": 1976,
 "numberOfPages": 504,
 "subjects": ["Dune (Imaginary place)", "Fiction", "Fiction in English", "Science Fiction"],
 "languages": ["pol", "chi", "ger", "eng", "spa", "dut", "fre", "por"],
 "coverUrl": "https://covers.openlibrary.org/b/id/6976407-L.jpg",
 "editionCount": 77,
 "ratingsAverage": 3.96,
 "ratingsCount": 47,
 "editions": [
 {
 "editionKey": "/books/OL47362500M",
 "title": "Dzieci Diuny",
 "publishDate": "Apr 14, 2020",
 "publishers": ["Rebis"],
 "isbn10": ["8381880475"],
 "isbn13": ["9788381880473"],
 "numberOfPages": 528,
 "physicalFormat": "hardcover",
 "languages": ["pol"]
 }
 ],
 "scrapedAt": "2026-08-06T12:00:00.000Z"
}
```

### Works versus editions

Open Library models a book at two levels, and the difference matters when you join data:

| Level | Key looks like | What it is |
|---|---|---|
| Work | `/works/OL893516W` | The abstract book. One row per title, regardless of printing. |
| Edition | `/books/OL47362500M` | One physical or digital printing, with its own ISBN, publisher and page count. |

Every dataset item is a work. `isbn10`, `isbn13`, `publishers` and `languages` are unions across all its editions, which is why a single record can carry hundreds of ISBNs. Set `includeEditions: true` when you need each printing separated out.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~openlibrary-books-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "isbns": ["9780441013593"],
 "includeEditions": true,
 "maxEditionsPerBook": 5
 }'
```

### JavaScript example

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/openlibrary-books-scraper').call({
 searchQueries: ['dune'],
 subjects: ['science fiction'],
 maxResults: 50,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const book of items) console.log(`${book.title} - ${book.authors.join(', ')} (${book.firstPublishYear})`);
```

### Notes

- One bad query won't kill the run. A failed query is recorded in `RUN_SUMMARY.failures` and the Actor moves on. It only errors out if every query failed and nothing was saved, because a run that saved nothing must never look like a run that found nothing.
- A query that matches nothing is not an error, but it is not silence either: you get zero rows and the query is named in `RUN_SUMMARY.targetsWithoutMatches`.
- `maxResults` is the most rows a run can save, so it is also the most a run can cost. Rows are only charged when they are saved: duplicates, unusable inputs and failed queries are not.
- The fields you get back are the fields the Actor asks for. The default projection is thin, so this Actor requests an explicit 15-column field list.
- Subjects are capped at 50 per book. Popular works carry hundreds of near-duplicate headings, and the first 50 are the useful ones.
- Public data only. Open Library's bibliographic data is published under CC0. Covers have their own terms, so this Actor returns cover URLs rather than image bytes.

### FAQ

**Do I need an Open Library account or key?** No. You supply no credentials.

**Why does one record have 248 ISBNs?** Because it is a work, not an edition. Those are the ISBNs of all 160 printings. Turn on `includeEditions` to see which ISBN belongs to which printing.

**Can I search by author?** Yes, put `author:le guin` in `searchQueries`. The full Open Library search syntax is passed through unchanged.

**Is `maxResults` a run total or a per-query cap?** A run total. Five queries at `maxResults: 100` save 100 books between them, not 500. Raise it when you want more.

**What happens if I send `maxResults: 0` or `-5`?** The run stops with an error naming the field, before anything is fetched or charged. There is no "unlimited" value: an out-of-range cap used to be quietly rewritten into a different one, which meant a bill nobody agreed to.

**Why did a query return nothing when `maxResults` was still high enough?** Check `RUN_SUMMARY`. `targetsWithoutMatches` means it ran and matched nothing; `targetsSkipped` means the run total was already spent before that query was reached; `failures` means it errored.

**Why is `ratingsAverage` null?** Most works have no community ratings. Rather than reporting a fake `0.0`, this Actor returns `null` when `ratingsCount` is zero.

**Can I plug it into something else?** Yes. Apify API, the client libraries, webhooks, scheduled runs, dataset exports to JSON, CSV or Excel, or MCP. The output is structured JSON.

# Actor input Schema

## `searchQueries` (type: `array`):

Free-text searches over title, author and full text, using the same syntax as the Open Library search box. Leave empty if you are only looking up ISBNs or subjects.

## `isbns` (type: `array`):

ISBN-10 or ISBN-13 values to look up directly. Hyphens and spaces are stripped automatically. Each ISBN resolves to the work that edition belongs to, so you get the full work record, not just that one printing.

## `subjects` (type: `array`):

Open Library subject headings, for example "science fiction" or "machine learning". Multi-word subjects are quoted for you. Leave empty to skip subject browsing.

## `includeEditions` (type: `boolean`):

Attach a list of individual printings: publisher, publish date, ISBNs, page count and physical format. Costs one extra request per book, so it roughly doubles run time.

## `maxEditionsPerBook` (type: `integer`):

Cap the editions attached to each book. Only used when "Include editions" is on. Popular titles have hundreds of printings. This bounds the size of the edition list inside a row; it does not change how many books a run saves — that is "Max books per run".

## `maxResults` (type: `integer`):

Hard ceiling on the books saved by the whole run, across every search query, ISBN and subject together. The allowance is shared evenly between your targets, and whatever one target does not use passes to the next. This is the most rows a run can ever save, so it is also the most it can ever cost.

## Actor input object example

```json
{
  "searchQueries": [
    "the left hand of darkness",
    "author:le guin"
  ],
  "isbns": [
    "9780441013593",
    "0-441-01359-0"
  ],
  "subjects": [
    "science fiction",
    "cryptography"
  ],
  "includeEditions": false,
  "maxEditionsPerBook": 10,
  "maxResults": 20
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# 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 = {
    "searchQueries": [
        "dune"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/openlibrary-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 = { "searchQueries": ["dune"] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/openlibrary-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 '{
  "searchQueries": [
    "dune"
  ]
}' |
apify call arman-bd/openlibrary-books-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/openlibrary-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/RMSyxS8ZH0UAOkrvC/builds/Dc6ORqrB9nolWXV6W/openapi.json
