Files
magnus919_agent-skills/openlibrary/references/api-overview-and-key-graph.md
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

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-* 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): 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.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).
  • 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):

{"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