Skip to content

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.

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/mcp from 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.

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.

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.

GET /api/extractions/{extraction_id}/canvases?offset=0&limit=50&query=f.%2012

The same paging contract as list_canvases: returned / totalCanvases / hasMore.

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.

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, or x,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.

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.

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.