Files
magnus919_agent-skills/openlibrary/references/api-overview-and-key-graph.md
T
Magnus Hedemarkandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> ce95392915 docs(openlibrary): add API references for key graph, search, ISBN, recipes
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>
2026-08-26 08:43:13 -04:00

176 lines
8.5 KiB
Markdown

# 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](search-api-guide.md)), ISBN/Books/Covers endpoints
([books-isbn-and-covers.md](books-isbn-and-covers.md)), and worked recipes
([recipes-and-gotchas.md](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](https://openlibrary.org/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-*` or `Retry-After` headers 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.json` batch 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](https://openlibrary.org/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](https://openlibrary.org/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.works` is 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>"}}]`.
Read `work.authors[].author.key`, never `work.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](books-isbn-and-covers.md)).
- Common record furniture: `type.key` (`/type/edition`, `/type/work`,
`/type/author`), integer-array `covers` / `photos`, `created`/`last_modified`
timestamps, `revision`/`latest_revision` integers.
## 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`):
```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:
```json
"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](https://openlibrary.org/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](https://openlibrary.org/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