Every stage before this one exists to earn a record the right to be seen. Ingestion validated it, the taxonomy layer persisted and resolved it, and rights automation decided it may be published. The delivery layer is where that accumulated work becomes a pixel on a scholar’s screen — a deep-zoomable image, a page-turnable manuscript, a structured tour through a multi-part object — served over the International Image Interoperability Framework (IIIF) so that any conformant viewer, anywhere, renders it identically. This is the downstream stage the rest of the platform hands off to, and it is unforgiving in a specific way: a manifest that is 99% correct does not degrade gracefully, it fails to load. Conformance is binary, so the discipline of the earlier stages has to carry all the way through to the JSON a viewer fetches.

Three institutional roles converge here. The collections manager owns the reader-facing result: the label a visitor sees, the attribution line that must appear, and the guarantee that an embargoed object never acquires a resolvable manifest. The Python developer owns the builders that turn a persisted record into a valid Presentation API 3.0 document and wire the Image API service that streams tiles. The DAMS administrator owns the operational surface — tile cache behaviour, edge invalidation, and the rights gate that must sit in front of every generated resource. This overview defines the reference architecture that satisfies all three and indexes the deeper guides that implement each part. It receives its inputs from the record ingestion and sync stage, reads the object shape defined by designing museum object schemas, and enforces the publication decisions made in rights metadata mapping and licensing automation.

Architecture Overview

IIIF splits cleanly into two cooperating specifications, and the delivery architecture mirrors that split. The Presentation API describes the intellectual structure of an object — its label, its canvases, its reading order, its rights — as a JSON document called a manifest. The Image API describes a service that returns arbitrary regions and sizes of a single source image on demand, so a viewer can request exactly the tiles it needs to fill a viewport instead of downloading a multi-hundred-megapixel master. A manifest never embeds pixels; it points at Image API services, and the viewer negotiates the rest. That separation is what lets a single high-resolution capture back a thumbnail, a full-object view, and a deep-zoom inspection without ever re-exporting the file.

The critical architectural rule is that a rights-enforcement gate sits in front of both. A persisted record only enters the manifest builder if the rights policy resolved at the previous stage marks it publish-eligible; a restricted or embargoed object is diverted before any resolvable resource is minted, so there is no manifest URL to leak. The diagram below shows the full stage this overview covers, from persisted record to rendered viewer.

IIIF delivery pipeline from persisted record to viewer A persisted record carrying resolved URIs and a LIDO blob enters a rights policy gate. Publish-eligible records flow right to the IIIF manifest builder emitting Presentation API 3.0, then to the Image API service serving tiles and info.json, then to a conformant IIIF viewer such as Mirador or Universal Viewer. Access-restricted or embargoed records branch downward and are never assigned a resolvable manifest. IIIF delivery — from persisted record to rendered viewer Persisted record resolved URIs · LIDO Rights policy gate publish-eligible? IIIF manifest builder Presentation 3.0 Image API service tiles · info.json IIIF viewer Mirador · UV Access-restricted embargoed · withheld eligible restricted

Every arrow is a contract. The gate arrow enforces that publication policy is honoured before a resource exists; the builder arrow enforces Presentation 3.0 structure; the service arrow enforces that image bodies point at a live Image API endpoint rather than a static file. The sections that follow specify the standards each contract must honour, the canonical Python that builds a conformant manifest, and the deeper guides for manifests, the Image API, and collection structuring.

Standards Landscape

IIIF delivery does not stand alone — a manifest is where half a dozen heritage standards surface simultaneously in front of the public. The descriptive fields come from the object’s LIDO record, the labels resolve against Getty vocabularies, the rights come from RightsStatements.org or Creative Commons, and the whole document is shaped by the two IIIF APIs. Getting the version boundaries right is the entire game: IIIF 3.0 restructured manifests enough that a 2.x-shaped document silently misbehaves in a 3.0 viewer. The table below maps each standard to where it is enforced in this stage and the concrete role it plays.

Standard Version Enforced at stage Use in this workflow
IIIF Presentation API 3.0 Manifest builder Structure of the manifest, canvas, and annotation graph; language maps, rights, and requiredStatement
IIIF Image API 3.0 Image API service info.json service description, tile pyramid, and region/size/rotation/quality request syntax
LIDO 1.1 Field mapping (builder input) Source of descriptive fields — title, creator, date, measurements — flattened into manifest metadata
RightsStatements.org 1.0 Rights gate + rights field Machine-readable rights URI asserted on the manifest and enforced before publication
Getty AAT / TGN Linked Open Data (current) Label resolution Human-readable labels for materials, subjects, and place names carried into manifest metadata entries
CIDOC CRM 7.1.3 Field mapping Event and provenance semantics behind provenance and production metadata surfaced in the manifest

Two version rules dominate operations. First, the rights value in a Presentation 3.0 manifest must be a single URI drawn from the RightsStatements.org or Creative Commons namespaces — a free-text copyright string is invalid there and belongs in requiredStatement instead. Translating a stored license into the right slot is the job covered in mapping Creative Commons licenses to IIIF rights. Second, every human-readable string in a 3.0 manifest is a language map — a JSON object keyed by BCP 47 language tag whose values are arrays of strings — never a bare string. That single structural change is the most common reason a manifest flattened from a 2.x mental model fails to render.

Core Implementation Pattern

The canonical task for this stage is a manifest builder: take one publish-eligible record and emit a valid Presentation 3.0 Manifest document. The pattern below models the input as a strict Pydantic v2 model so a malformed record fails before it can produce a malformed manifest, then constructs the manifest as a plain dict ready to serialize. It is deliberately minimal — one canvas, one image — because the structural obligations (language maps, the annotation graph, the rights URI, the requiredStatement) are identical whether an object has one canvas or four hundred. Note the Python 3.9+ syntax (str | None) and Pydantic v2 API (model_config, field_validator, model_validate).

python
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, field_validator

BASE_URI = "https://iiif.example-museum.org"


class ObjectRecord(BaseModel):
    """A publish-eligible record handed to the IIIF delivery stage."""
    model_config = ConfigDict(strict=True, extra="forbid")

    identifier: str = Field(pattern=r"^[A-Z]{2,4}\.\d{4}\.\d{1,4}$")
    label: str = Field(min_length=1, max_length=500)
    summary: str | None = None
    image_id: str                       # Image API service base URI
    width: int = Field(gt=0)
    height: int = Field(gt=0)
    rights_uri: str = Field(
        pattern=r"^https://(rightsstatements\.org|creativecommons\.org)/"
    )
    attribution: str
    language: str = "en"

    @field_validator("image_id")
    @classmethod
    def strip_trailing_slash(cls, v: str) -> str:
        # Image API service IDs must not end in a slash.
        return v.rstrip("/")


def lang_map(value: str, language: str) -> dict[str, list[str]]:
    """IIIF 3.0 language map: values are wrapped by a BCP 47 tag."""
    return {language: [value]}


def build_manifest(record: ObjectRecord) -> dict[str, Any]:
    """Emit a valid Presentation API 3.0 Manifest for one object."""
    manifest_id = f"{BASE_URI}/iiif/manifest/{record.identifier}"
    canvas_id = f"{manifest_id}/canvas/1"
    page_id = f"{canvas_id}/page/1"
    anno_id = f"{page_id}/annotation/1"
    service_id = record.image_id

    manifest: dict[str, Any] = {
        "@context": "http://iiif.io/api/presentation/3/context.json",
        "id": manifest_id,
        "type": "Manifest",
        "label": lang_map(record.label, record.language),
        "rights": record.rights_uri,
        "requiredStatement": {
            "label": lang_map("Attribution", record.language),
            "value": lang_map(record.attribution, record.language),
        },
        "items": [
            {
                "id": canvas_id,
                "type": "Canvas",
                "label": lang_map("Recto", record.language),
                "height": record.height,
                "width": record.width,
                "items": [
                    {
                        "id": page_id,
                        "type": "AnnotationPage",
                        "items": [
                            {
                                "id": anno_id,
                                "type": "Annotation",
                                "motivation": "painting",
                                "target": canvas_id,
                                "body": {
                                    "id": f"{service_id}/full/max/0/default.jpg",
                                    "type": "Image",
                                    "format": "image/jpeg",
                                    "height": record.height,
                                    "width": record.width,
                                    "service": [
                                        {
                                            "id": service_id,
                                            "type": "ImageService3",
                                            "profile": "level1",
                                        }
                                    ],
                                },
                            }
                        ],
                    }
                ],
            }
        ],
    }
    if record.summary is not None:
        manifest["summary"] = lang_map(record.summary, record.language)
    return manifest


if __name__ == "__main__":
    import json

    record = ObjectRecord.model_validate(
        {
            "identifier": "PMA.1928.42",
            "label": "Sunflowers",
            "summary": "Oil on canvas, 1889.",
            "image_id": "https://iiif.example-museum.org/iiif/3/PMA.1928.42",
            "width": 4000,
            "height": 5200,
            "rights_uri": "https://creativecommons.org/publicdomain/zero/1.0/",
            "attribution": "Example Museum of Art — Public Domain",
        }
    )
    print(json.dumps(build_manifest(record), indent=2))

The ObjectRecord model is the enforcement point: strict=True blocks silent coercion, extra="forbid" rejects a drifted record carrying fields the builder does not map, and the rights_uri pattern guarantees the manifest cannot be built without a URI drawn from a recognised rights namespace. The lang_map helper is what keeps every reader-facing string 3.0-conformant, and the image body points at an ImageService3 service rather than a static file — which is what lets the viewer request tiles instead of the master. Everything else in this section scales that single-canvas skeleton: more canvases for multi-page objects, richer metadata from LIDO, and structures for navigable ranges.

Workflows in This Section

Each delivery concern has its own dedicated guide. Together they implement the two IIIF APIs and the structural layer that binds objects into browsable wholes.

Building the manifests. The building IIIF presentation manifests guide is the deepest treatment of the Presentation 3.0 document: the canvas and annotation graph, language maps, metadata entries crosswalked from LIDO, and thumbnail generation. Its child guides go further on generating IIIF v3 manifests with Python, which scales the builder above to multi-canvas objects, and on adding rights and requiredStatement blocks to IIIF manifests, which pins down exactly which rights URI goes in rights and which attribution text goes in requiredStatement.

Serving the pixels. The configuring the IIIF Image API guide covers the service half: the info.json description, compliance levels, and the region/size/rotation/quality request grammar that every image body in a manifest depends on. Its child guides address the two operational realities of image serving — serving deep-zoom IIIF tiles with pyvips for building the tile pyramid efficiently, and caching IIIF Image API responses at the edge for keeping a high-traffic collection fast without hammering the origin.

Structuring the whole. A single manifest describes one object, but collections are browsable graphs. The structuring IIIF collections and ranges guide covers Collection documents that bind manifests into navigable sets and Range structures that model reading order within a manifest. Its child guide, modeling multi-volume objects as IIIF ranges, works through the case that trips most institutions up — a bound multi-volume work whose canvases must group into volumes and chapters.

Integration Boundaries

This stage consumes; it does not decide. Understanding its boundaries prevents responsibility from bleeding across the pipeline. Upstream, the boundary is the publish-eligibility decision: the delivery layer treats a record’s descriptive fields, resolved authority URIs, and rights policy as already settled, and its job is faithful exposure, not adjudication. If a manifest carries the wrong label, the fix belongs in the taxonomy layer’s designing museum object schemas, not in the builder. If an object appears that should be embargoed, the fix belongs in rights metadata mapping and licensing automation, because the rights gate here only enforces a decision made there.

The diagram below places this stage at the end of the four-stage handoff. Record ingestion produces a validated record; the core architecture stage persists it and resolves its authority URIs; rights automation attaches a publication policy; and only then does IIIF delivery — this stage — expose it. Keeping delivery ignorant of everything but the eligibility flag is what lets a full re-publish run safely: it can regenerate every manifest in the collection without ever re-exposing material the rights stage has since withdrawn.

Four-stage handoff ending at IIIF delivery Four stages left to right. Ingestion and sync hands a validated record to Taxonomy and persistence in the core architecture stage, which hands resolved URIs to Rights and licensing policy, which hands a publish-eligible record to IIIF delivery. IIIF delivery is this section and is the terminal consumer that exposes objects to viewers. Stage handoffs — IIIF delivery is the terminal consumer INGESTION Ingestion & sync CORE ARCHITECTURE Taxonomy & persistence RIGHTS & LICENSING Rights & licensing policy THIS SECTION IIIF delivery (this stage) validated record resolved URIs publish-eligible This stage exposes objects to viewers — it enforces publication policy but never decides it.

Operational Checklist

Before promoting an IIIF delivery layer to production, confirm every gate below. These checks separate a manifest that renders everywhere from one that fails in the first viewer a reader opens.

  • The rights gate runs before any manifest is minted, so an embargoed or restricted object never acquires a resolvable manifest URL.
  • Every manifest, canvas, annotation page, and annotation carries a globally unique, dereferenceable id, and every type matches the 3.0 vocabulary (Manifest, Canvas, AnnotationPage, Annotation).
  • All reader-facing strings — label, summary, requiredStatement, metadata — are 3.0 language maps keyed by a BCP 47 tag, never bare strings.
  • The rights value, when present, is a single URI from the RightsStatements.org or Creative Commons namespaces; attribution text lives in requiredStatement, not in rights.
  • Every image annotation body points at an ImageService3 service, and each service resolves a valid info.json with matching width/height.
  • Image API compliance level is declared in info.json and matches what the tile server actually supports (region, size, rotation, quality, format).
  • Canvas height/width match the source image dimensions so viewers compute the correct aspect ratio and tile grid.
  • Multi-part objects declare reading order via structures ranges rather than relying on canvas array order alone.
  • Generated manifests pass an external IIIF validator in CI before deploy, not just a local schema check.
  • Tile and info.json responses carry cache headers and a documented edge-invalidation path for when a record is corrected or withdrawn.

Failure Modes

The failures below recur across institutions moving to IIIF. Each has a deterministic remediation and a guide that treats it in depth.

Failure pattern Root cause Remediation
Manifest loads blank or errors in every viewer Reader-facing strings emitted as bare strings instead of 3.0 language maps Wrap all strings in language maps; see building IIIF presentation manifests
Validator rejects the rights value A free-text copyright string placed where only a rights URI is allowed Move attribution to requiredStatement, put a URI in rights; see adding rights and requiredStatement blocks
Image never loads though the manifest parses Body points at a static file, not a live ImageService3 endpoint Serve an Image API service with info.json; see configuring the IIIF Image API
Deep zoom stutters or times out under load No pre-built tile pyramid, so the origin re-derives regions per request Pre-tile with pyvips; see serving deep-zoom IIIF tiles with pyvips
Multi-volume object renders as one flat page sequence Volumes and chapters not modeled as ranges Add structures ranges; see modeling multi-volume objects as IIIF ranges
A withdrawn object stays visible after correction Edge cache served a stale manifest or tile after the record changed Invalidate on change; see caching IIIF Image API responses at the edge