Service API
Everything on this page is a REST equivalent of an MCP tool, for callers that are servers rather than assistants. If you are connecting Claude or ChatGPT, you want Tools instead.
Why it exists
Section titled “Why it exists”The MCP endpoint cannot be driven from another server. Two reasons, both deliberate:
- FastMCP’s DNS-rebinding guard only accepts the public host name, so a call to
http://iiiftools-api:8000/mcpfrom a neighbouring container answers 421. - Image delivery over MCP hands back signed URLs minted against the public origin. An internal caller would be sent out through the proxy to collect a file already sitting on this server’s disk.
So a service-to-service caller gets plain REST, and gets bytes rather than links.
Authenticating
Section titled “Authenticating”Send the shared key as a header:
X-IIIFTools-Service-Key: <SERVICE_API_KEY>The same routes also accept a normal Authorization: Bearer <token> from a signed-in
person, so the frontend can use them unchanged.
Two things to know. A key that is presented but wrong is refused outright — the request is
never retried as anonymous. And if SERVICE_API_KEY is unset on the server, the service path
is closed rather than open: no credential configured means no service access.
The key travels over the public /api/ surface exactly as a bearer token does, so treat it
as a credential, not as a network boundary: 32+ random bytes, kept in the secret store,
rotated like anything else.
Routes
Section titled “Routes”Resolve
Section titled “Resolve”POST /api/resolve{ "url": "https://digital.nls.uk/126160669", "offset": 0, "limit": 25 }Returns the extraction summary (extraction_id, manifest_url, label, iiif_version,
canvas_count, crop_hint), the matched repository slug, manifest metadata, and the
first page of canvases. A URL that cannot be resolved answers 422 with the resolver’s own
explanation — which, for a hub page, lists the viewer links it did find.
Store manifest_url, not extraction_id. Extractions expire on a TTL (two hours by
default) and their cached manifests are swept with them; the manifest URL is stable, so a
consumer that wants to remember a document should keep that and resolve again when it next
needs pixels.
Canvases
Section titled “Canvases”GET /api/extractions/{extraction_id}/canvases?offset=0&limit=50&query=f.%2012The same paging contract as list_canvases: returned / totalCanvases / hasMore.
Tile grid
Section titled “Tile grid”POST /api/extractions/{extraction_id}/tiles{ "canvas_index": 41, "region": "full", "tile_px": 1024, "overlap": 64 }Coordinates only — nothing is downloaded. Returns the grid (rows, cols, totalTiles)
and every tile’s native-pixel x/y/w/h in reading order.
Ask for the grid whenever the point is to read rather than to look: a folio downscaled to fit a frame is illegible for dense script, and tiles are how you get 1:1 pixels.
Image bytes
Section titled “Image bytes”POST /api/extractions/{extraction_id}/image{ "canvas_index": 41, "region": "full", "size": null, "enhance": null, "native_tile": false }Responds with the image itself (image/jpeg), plus X-IIIF-Canvas-Index,
X-IIIF-Canvas-Label, X-IIIF-Region, X-IIIF-Size, X-IIIF-Width, X-IIIF-Height.
region—full,crop_hint, orx,y,w,h.enhance—gray,stretch,clahe,clahe_red(red-channel CLAHE, best for brown iron-gall ink).native_tile: true— the region is one tile from the grid above. This picks the tile path, which tries an IIIF region request first and falls back to cropping a cached full download. The fallback is not decorative: some repositories (NLS among them) refuse certain region/size combinations outright.
Images are cached per extraction and deduplicated by request, so asking twice costs one download.
Release
Section titled “Release”DELETE /api/extractions/{extraction_id}Gives the disk back rather than waiting for the sweeper. Scoped to the owner, so you can only release your own — and a service caller owns what it resolved.
What has not changed
Section titled “What has not changed”Extraction listing stays OAuth-only, every extraction route is scoped to the calling user, images are still swept on the extraction TTL, and nothing here widens what a signed URL can serve.