Four cited reference files distilled from live-verified research against openlibrary.org developer docs: OL...M/W/A key graph with merge-stub behavior, search query syntax and error model, ISBN 302 redirect resolution and covers host rules, plus worked curl/jq pipelines with a symptom-indexed gotcha table. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
8.5 KiB
Open Library API Overview and the OLID Key Graph
Open Library (an Internet Archive project) exposes its catalog of 50M+ records through public, keyless HTTP APIs. This file covers the access model, etiquette, and the record-key graph that everything else builds on. Companion files: the Search API (search-api-guide.md), ISBN/Books/Covers endpoints (books-isbn-and-covers.md), and worked recipes (recipes-and-gotchas.md).
Access model: no key, three hosts, open CORS
Reads require no API key and no registration. Three hostnames matter:
| Host | Serves |
|---|---|
openlibrary.org |
Metadata JSON: search, works, editions, authors, ratings |
covers.openlibrary.org |
Cover images and author photos (separate service) |
archive.org |
Ebook/scan content and bulk dumps (redirect targets) |
Metadata endpoints answer application/json, support GET + OPTIONS, and send
access-control-allow-origin: *, so browser-side fetches work directly.
Optional identification: put your app name and contact email in the User-Agent,
e.g. User-Agent: MyLibraryApp (contact@example.org). Identified traffic gets a 3x
rate allowance (see below) and gives staff someone to contact before blocking you.
There is no token, secret, or account step anywhere in the read surface.
Rate limits and etiquette (official guidance)
From the official APIs index page (developers/api):
- Anonymous clients: 1 request/second. Identified clients (User-Agent carrying app name + contact email/phone): 3 requests/second ("identified requests will enjoy a 3x request limit").
- No
X-Rate-Limit-*orRetry-Afterheaders are sent today; the limit is advisory policy, not header-signaled. Exceeding it politely means sleeping, because there is nothing to read out of the response. - Explicitly discouraged: HTML scraping (use the API endpoints), spreading traffic
across 5+ IPs, bulk harvesting, hundreds of single-book GETs where one
/search.jsonbatch would do, or using Open Library as a backend for a high-traffic service. Violations bring "aggressive rate limiting or blocking". - For bulk data use the monthly dumps instead
(developers/dumps): editions ~9.2G,
works ~2.9G, authors ~0.5G, ratings/reading-log much smaller. Dump lines are
type, key, revision, last_modified, JSON.
The covers service has its own harder limit: non-ID/non-OLID cover lookups are capped at 100 requests/IP per 5 minutes, then 403 Forbidden (dev/docs/api/covers).
The key graph: OLIDs and the letter-suffix type system
Every catalog entity has a stable-shaped identifier called an OLID whose final letter encodes the type:
| Suffix | Type | Canonical JSON path | Example |
|---|---|---|---|
M |
Edition (a physical/digital publication) | /books/OL34854896M.json |
OL34854896M |
W |
Work (the abstract creative work) | /works/OL45804W.json |
OL45804W |
A |
Author | /authors/OL23919A.json |
OL23919A |
Two surface forms appear in payloads and docs alike: bare OLIDs (OL45804W) and
path keys (/works/OL45804W). Search results return path keys for works
(/works/OL…W) but bare keys in author search (OL…A). Parse defensively: strip or
add the collection prefix by inspecting the suffix letter rather than assuming one form.
Graph wiring, verified against live records:
- Edition → work:
edition.worksis a list of key refs:"works": [{"key": "<RECORD_KEY>"}]. - Work → authors: double-nested, with a role node:
"authors": [{"author": {"key": "<RECORD_KEY>"}, "type": {"key": "<RECORD_KEY>"}}]. Readwork.authors[].author.key, neverwork.authors[].key. - Edition → authors: flat single nesting instead:
"authors": [{"key": "<RECORD_KEY>"}]. The two collections disagree — handle both. - Work → editions: not embedded; enumerate via
/works/OL…W/editions.json(see books-isbn-and-covers.md). - Common record furniture:
type.key(/type/edition,/type/work,/type/author), integer-arraycovers/photos,created/last_modifiedtimestamps,revision/latest_revisionintegers.
Wrong key type: cross-collection 301 reroutes
Requesting a key under the wrong collection is forgiven with a redirect to the right one (live-verified):
GET https://openlibrary.org/books/OL23919A.json # author OLID under /books
HTTP/2 301
location: https://openlibrary.org/authors/OL23919A.json
GET https://openlibrary.org/works/OL123M.json # edition OLID under /works
HTTP/2 301
location: https://openlibrary.org/books/OL123M.json
So a client that follows redirects survives suffix/collection mismatches automatically
(requests follows by default; curl needs -L). The practical symptom of not
following: your parser sees an HTML 301 page instead of JSON.
Missing keys and the merge problem: redirect stubs inside HTTP 200
Truly nonexistent keys 404 with a JSON body:
GET https://openlibrary.org/books/OL999999999M.json
HTTP/2 404
{"error": "notfound", "key": "<RECORD_KEY>"}
But Open Library is a wiki: duplicates are merged and spam is deleted, and merged
keys do not 3xx. A merged-away key keeps serving HTTP 200 with a stub record
(live-verified on a real merge found via /recentchanges/merge-works.json):
{"location": "<RECORD_KEY>",
"type": {"key": "<RECORD_KEY>"},
"latest_revision": 4, "revision": 4, ...}
The old key /works/OL24776360W had just been merged into /works/OL14868272W, yet
the JSON endpoint returned 200, not 302. Clients must detect
payload["type"]["key"] == "/type/redirect" in successful responses and re-fetch
payload["location"] themselves. (The HTML page for the same key does 302; only JSON
gives you the stub.) The bundled CLI performs this follow-up automatically.
Related identity hazards:
- Deleted-and-reassigned keys: an OLID freed by deletion can be reissued for an unrelated book (observed live). Never treat an OLID as long-term identity for caching; pair it with title or ISBN.
- Recent merges are observable at
/recentchanges/merge-works.json?limit=N(data.master,data.duplicates[]) if you need to audit drift.
Text-valued fields nest {type, value} objects
Free-text fields (bio on authors; description, notes, first_sentence on
editions/works) arrive in two shapes depending on record age:
"bio": {"type": "/type/text", "value": "Joanne \"Jo\" Murray, OBE ..."}
Older records carry a plain string instead. Always branch: if dict, take ["value"];
if str, use as-is. The bundled CLI unwraps these automatically.
Photo/cover ID arrays mix in -1 placeholders meaning "no image"
(e.g. "photos": [5543033, -1]): filter out negatives before building image URLs.
Author URL quirk: .json placement matters
Bare author URLs HTML-redirect to slugged pages
(/authors/OL23919A → /authors/OL23919A/J._K._Rowling). Appending .json after
the slug (/authors/OL23919A/J._K._Rowling.json) does not serve JSON — append
it to the bare key: /authors/<AUTHOR_KEY>.json
(dev/docs/api/authors).
Writes exist but are outside the keyless surface
Authenticated writes exist (POST /account/login.json with Internet Archive S3 keys
returns a session cookie; PUT resource JSON updates records), but the RESTful doc
states this is effectively internal: PUT/POST without permission returns 403, and
the docs warn the API "works only from the localhost"
(dev/docs/restful_api). Plan around
reads only; expect every write path to require credentials this skill deliberately
does not handle.
Sources
- https://openlibrary.org/developers/api — API index; rate limits (1/s anonymous, 3/s identified), User-Agent identification format, bulk-access policy
- https://openlibrary.org/dev/docs/api/authors — Authors API; slug/
.json-placement rule - https://openlibrary.org/dev/docs/api/books — Books/Editions/Works API; record shapes
- https://openlibrary.org/dev/docs/restful_api — write/login mechanics, status codes, localhost-only caveat
- https://openlibrary.org/developers/dumps — monthly dump catalog and line format
- Live read-only probes against openlibrary.org (2026-08-26): 301 cross-collection reroutes, 404 body shape, merge-stub 200 responses,
{type,value}text nesting