# IIIF Publication Preflight (`l3digital/iiif-publication-preflight`) Actor

Experimental bounded IIIF Presentation 3 image-resource and metadata diagnostics attributed to canvases.

- **URL**: https://apify.com/l3digital/iiif-publication-preflight.md
- **Developed by:** [L3Digital](https://apify.com/l3digital) (community)
- **Categories:** Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.05 / useful iiif report

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## IIIF Publication Preflight

Get an experimental dependency report for one IIIF Presentation 3 Manifest with
embedded annotation pages. The report joins HTTP, CORS and limited Image API
metadata observations to the affected canvases. Use it to locate missing or
restricted image dependencies and metadata problems before investigating them in
a viewer. It does not check image pixels, viewer behavior or full IIIF conformance.

### Use from an AI agent through MCP

Add this URL to a client that supports remote HTTP MCP, then authorize with your
own Apify account using the [Apify MCP setup guide](https://docs.apify.com/integrations/mcp):

```text
https://mcp.apify.com/?tools=l3digital/iiif-publication-preflight
```

This configuration selects the Actor directly. Availability still depends on
Apify account and Actor eligibility. Call `l3digital/iiif-publication-preflight`
with the example input below; this page's pricing and input restrictions apply.

When using `call-actor`, its response contains run status and storage IDs. If the
run is still active, check that run with `get-actor-run`. After success, retrieve
the report with `get-dataset-items` using the returned dataset ID
(`defaultDatasetId` in the run API). These retrieval tools load with the Actor.
Retrieve the existing result instead of starting another run, which can incur
another report charge. Inspect `coverage`, resource `access`, `robots` and
`diagnostics` before using the observations.

### Input

Submit one public HTTPS manifest URL. Optionally supply the HTTPS origin of your
viewer to observe credentialless CORS responses for that origin.

For a public reference example, use:

```json
{
  "manifestUrl": "https://iiif.io/api/cookbook/recipe/0008-rights/manifest.json",
  "viewerOrigin": "https://example.org"
}
```

This example uses the IIIF Cookbook's [rights recipe](https://iiif.io/api/cookbook/recipe/0008-rights/),
credited to Glen Robson, IIIF Technical Coordinator, under
[CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/).
A hosted owner check of this reference produced complete coverage for three
resources, with four HTTP attempts including robots, allowed CORS for the supplied
origin and valid ImageService3 metadata. These are reference observations, not
institutional coverage, visual validation or IIIF endorsement. Future responses
can change. A complete useful report from this example qualifies for the **$0.05**
charge described below; it is not a free hosted demo.

`manifestUrl` must be caller-authorized public HTTPS without credentials, query,
fragment or a custom
port. `viewerOrigin` is optional and must be an HTTPS origin without an application
path. Runtime validation rejects unsafe DNS answers and checks robots before each
resource/redirect. No cookies, credentials or environment proxy settings are sent.
DNS resolution failure is inconclusive (`dns_unresolved`); private or mixed DNS
answers are refused. Origin robots policies and acquisition failures are cached
for one report, preserving the first diagnosis without repeated failed attempts.
Requests identify as `IIIFPublicationPreflight`. Robots groups match case-folded
prefixes of the product token before any `/version`; suffix/version substrings
do not override wildcard policy.
Robots decoding accepts a leading UTF-8 BOM and refuses invalid UTF-8. Empty
Allow/Disallow directives end a group's user-agent list. This intentionally
corrects earlier handling that could merge an empty wildcard Disallow group with
the following unrelated bot's deny rules; those unrelated rules no longer apply
to this Actor. Both group orders preserve the bot-specific policy.
Applicable `Crawl-delay` or `Request-rate` declarations make a source unsupported
for acquisition: this bounded Actor has no pacing scheduler. It conservatively
refuses protected requests for any declared value, including zero, empty or
malformed values. These extensions are not mandatory RFC 9309 directives; this
refusal honors the Actor's stronger source-policy contract. Only the selected
most-specific groups, including ties, impose this restriction; unrelated bot
groups and overridden wildcard groups do not. Robots reads are still permitted.
The report is partial, with resource `robots: unknown`, `access: refused` and
`robots_pacing_unsupported` in `diagnostics`; the refused source's manifest or
dependency is not contacted. Cached origins and redirect destinations receive
the same check. These partial reports do not charge a report event.
Use only sources you are authorized to inspect; public availability and robots
permission do not grant reuse rights. Confirm the source's terms and your intended
use before submitting a manifest. The reference example's license does not grant
permission to inspect or reuse material from other sources.

### Output and interpretation

The Actor writes one default dataset row and the same JSON to the default key-value
store's `OUTPUT` record. Open either output after the run to read the report.
Resources are deduplicated by URL and method; each retains source-order `canvasIds`.
Images receive HEAD only. Root manifests, service `info.json` and robots receive
bounded GET. HTTP 200 proves neither decodable image content nor correct tiles,
pixels or viewer behavior. Image API metadata checks only the documented subset:
object, identifier, type/protocol/profile and positive integer dimensions. Canvas
and image dimensions may legitimately differ.

| Field | Meaning |
| --- | --- |
| `observedAt` | UTC report start time |
| `coverage` | `complete` supported observations; `partial` omitted/unknown surfaces; `unsupported` root outside embedded P3 Manifest scope |
| `resources` | URL, GET/HEAD method, roles, affected canvases, final URL, status, access, robots, CORS and metadata diagnostics |
| `access` | Observed 200 `accessible`, 404/410 `missing`, 401/403 `restricted`; rate limits, server errors, HEAD 405 and transport failures `unknown`; rejected acquisition hops `refused` |
| `cors` | Observation for the supplied origin and actual GET/HEAD method, with no credentials; no universal browser compatibility claim |
| `findings` | Explicit unsupported, inconsistent or omitted surfaces; bounded code strings, with canvas IDs where available |
| `omittedResources`, `omittedFindings` | Counts of report omissions, making partial output explicit |
| `requestCount`, `byteCount` | HTTP attempts including robots/redirects, and downloaded JSON/robots body bytes |

`coverage: complete` means the supported observations were performed, not that
every dependency is healthy. Missing (404/410) or restricted (401/403) image HEAD
responses can be complete negative observations. A non-200 service `info.json`
response leaves metadata unchecked and makes coverage partial.

Malformed/repeated CORS headers, wrong origins and invalid metadata are concrete
observations. Restricted access is reported without retrying with credentials.
`refused` can concern the resource, its redirect or a required robots hop; it does
not establish that the original resource's address itself is unsafe.
Collections, Presentation 2, remote annotation pages, nonpainting annotations,
Choice/SpecificResource/non-image bodies, ambiguous targets and unsupported
services remain explicit unchecked surfaces. The Actor does not fetch remote pages,
perform migration, validate full IIIF conformance, decode images or run a viewer.

#### Reference output excerpt

This excerpt comes from the hosted reference check above. The complete report
also contains the manifest and Image API metadata resources; those entries and
other fields are omitted here for readability. `complete` describes coverage,
while image content and full conformance remain unchecked.

```json
{
  "reportVersion": 1,
  "coverage": "complete",
  "resources": [
    {
      "url": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg",
      "method": "HEAD",
      "roles": [
        "image"
      ],
      "canvasIds": [
        "https://iiif.io/api/cookbook/recipe/0008-rights/canvas/p1"
      ],
      "status": 200,
      "access": "accessible",
      "robots": "allowed",
      "cors": "allowed",
      "imageContent": "unchecked"
    }
  ],
  "requestCount": 4,
  "byteCount": 2711,
  "fullConformance": "unchecked"
}
```

### Bounds

The ceilings are 50 canvases, 100 URL/method resources including the manifest,
1,000 annotation traversal attempts, 128 HTTP attempts, 2 MiB downloaded
metadata, 20 JSON nesting levels, 20,000 JSON value nodes, 45 seconds inside the
worker, a 50-second process watchdog, three redirects per chain and 2,048-character
URLs/identifiers. Raw ASCII path characters requiring encoding must already be
percent-encoded; Unicode paths use UTF-8 percent encoding consistently with robots.
The normalized URL must also fit 2,048 characters before robots work or DNS.
Robots matching shares at most 100,000 work units per report, charging name and
pattern characters plus wildcard-loop iterations. Policies are parsed once per
origin. Over-limit policy remains unknown and protected acquisition is skipped.
This deterministic work cap is not a measured CPU-time guarantee.
One complex policy can exhaust the shared allowance and leave later resources,
including other origins, unknown. Coverage on such hosts is unmeasured.
Findings retain at most 100 entries and 100,000 UTF-8 bytes with explicit omitted counts.
The JSON report is at most 750,000 UTF-8 bytes. Whole resource
records are omitted when necessary; canvas attribution within retained records is
never truncated. Policy and acquisition-budget failures produce partial observations when the worker
returns. If the hard watchdog terminates the worker, or the isolated worker fails,
the SDK run fails with a fixed error and persists no report. Partial recovery from
a killed worker is not implemented.

### Initial test pricing

The initial test price is **$0.05 per useful report**, billed as one
`report-produced` event. There is no Actor startup or dataset charge. One event is
requested only after both the default dataset row and KVS `OUTPUT` are successfully written,
and only when `coverage` is `complete` with at least one image resource carrying
nonempty `canvasIds`. Complete negative observations such as missing (404/410),
restricted (401/403) images or invalid Image API metadata still qualify.

Partial, unsupported, empty and unattributed reports are persisted without this
event. Invalid input, worker failure/timeout and failed output writes do not request
a charge. Pay-per-event mode requires the configured event and available capacity
before acquisition. If a charge request fails, the run fails after output has been
persisted; whether the platform accepted that charge may be unknown. An unexpected
provider charge count also fails visibly. There is no automatic charge retry,
restart recovery or cross-run deduplication. Rerunning the same input can produce
another billable report.

### Local offline example

For developers with this repository, Python 3.13 and uv are required to use the
pinned SDK and model contracts. From the repository root, run the closed synthetic
example on the worker:

```sh
rexec --shell 'cd actors/iiif-publication-preflight && uv run --locked python -m iiif_publication_preflight.demo'
```

This command makes no network or platform calls and bypasses billing. It prints
one JSON report with `coverage: complete`, three HTTP fixture attempts, and one
missing image resource attributed to both `https://museum.example/c1` and
`https://museum.example/c2`. `imageContent` and `fullConformance` remain `unchecked`.
These synthetic results do not establish customer usage, source permission or demand.

See [PRODUCT.md](PRODUCT.md) and [VALIDATION.md](VALIDATION.md) for development
scope, test receipts and limitations. The package pins Apify 4.0.1 and Pydantic
2.11.9; the output schemas are generated from the report model.

# Actor input Schema

## `manifestUrl` (type: `string`):

One caller-authorized public HTTPS Presentation 3 Manifest without credentials, query, fragment or custom port. Runtime validates DNS and robots before acquisition.

## `viewerOrigin` (type: `string`):

Explicit HTTPS origin for credentialless CORS observations; no path, query, fragment or credentials.

## Actor input object example

```json
{}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `report` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("l3digital/iiif-publication-preflight").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("l3digital/iiif-publication-preflight").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call l3digital/iiif-publication-preflight --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,l3digital/iiif-publication-preflight"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/PaVOx4fg6Zj68uhCw/builds/ieIlzFlWEb0DG7aZU/openapi.json
