IIIF
Introduction
This tutorial explains how to use the Rijksmuseum IIIF APIs in a practical workflow for retrieving images, image metadata, and change data.
The documentation page explains the available endpoints and technical specifications. This tutorial focuses on something different:
How do you actually use the IIIF APIs in a real workflow?
After completing this tutorial, you will understand:
- what IIIF is and when to use it
- how to retrieve an image using the Image API
- how to apply transformations such as cropping, resizing, and rotation
- how to retrieve a manifest using the Presentation API
- how to track changes to published resources over time using the Change Discovery API
What is IIIF?
IIIF (International Image Interoperability Framework) is a set of open standards for delivering images and their metadata over the web. It allows images to be accessed and displayed in a consistent way across different platforms and viewers.
The Rijksmuseum implements three IIIF APIs:
- Image API — retrieve images and apply transformations (crop, resize, rotate)
- Presentation API — retrieve structured metadata describing a digital object
- Change Discovery API — track changes to resources over time
When should you use IIIF?
Use the IIIF APIs when you want to:
- display high-resolution images of artworks
- crop, resize, rotate, or convert images programmatically
- integrate Rijksmuseum images into a IIIF-compatible viewer
- track changes to manifests and metadata resources over time
IIIF is not the right choice when you need to:
- search for objects by metadata
- retrieve descriptive metadata such as title, creator, or date
In those cases, the Search API or OAI-PMH may be more appropriate.
How the IIIF APIs work
The three IIIF APIs serve different purposes:
- Image API — given an IIIF image identifier, construct a URL to retrieve the image with optional transformations
- Presentation API — given an identifier, retrieve a manifest describing the structure and composition of the digital object
- Change Discovery API — starting from a collection endpoint, paginate through change events to discover which resources and metadata representations were created, updated, or deleted
In simple terms:
- Image API → images
- Presentation API → structure and metadata
- Change Discovery API → synchronization and change tracking
Step-by-step tutorial
Step 1 — Find the image identifier
Before you can use the IIIF Image API, you need an image identifier. This identifier is not directly available on the object. Instead, it must be retrieved through a chain of three Linked Art resources.
a. Resolve the object record
Start with the Linked Art record of an object. For example, the Night Watch:
https://data.rijksmuseum.nl/200107928?_profile=la-framed
In the response, find the shows field, which contains a reference to the associated VisualItem:
"shows": [
{
"id": "https://id.rijksmuseum.nl/202107928",
"type": "VisualItem"
}
]
This links the object to a VisualItem, which links the object to its digital visual representation.
Important: all identifiers using id.rijksmuseum.nl must be resolved using the data.rijksmuseum.nl endpoint.
b. Resolve the VisualItem
Request this VisualItem identifier:
https://data.rijksmuseum.nl/202107928?_profile=la-framed
In the response, find the digitally_shown_by field, which contains a reference to a DigitalObject:
"digitally_shown_by": [
{
"id": "https://id.rijksmuseum.nl/500711199912110510799100",
"type": "DigitalObject"
}
]
This links the VisualItem to a DigitalObject (the actual media source).
c. Resolve the DigitalObject
Request this DigitalObject identifier:
https://data.rijksmuseum.nl/500711199912110510799100?_profile=la-framed
In the response, find the access_point field, which contains the IIIF image URL:
"access_point": [
{
"id": "https://iiif.micr.io/PJEZO/full/max/0/default.jpg",
"type": "DigitalObject"
}
]
This DigitalObject contains the IIIF image service URL.
The image identifier is the segment between iiif.micr.io/ and /full — in this case PJEZO. This identifier is used in all subsequent IIIF Image API requests.
Retrieving this identifier manually requires three separate requests and is not practical for multiple objects. The Finding the image identifier example in Step 6 automates this process.
Step 2 — Retrieve image metadata
You can request image metadata using the info.json endpoint:
https://iiif.micr.io/PJEZO/info.json
This returns a JSON document describing the image and its capabilities. The most relevant fields are:
{
"@context": "http://iiif.io/api/image/3/context.json",
"id": "https://iiif.micr.io/PJEZO",
"type": "ImageService3",
"profile": "level2",
"width": 14645,
"height": 12158,
"qualities": ["default", "gray", "color"],
"formats": ["jpg", "png", "webp"],
"tiles": [
{
"scaleFactors": [1, 2, 4, 8, 16],
"width": 1024,
"height": 1024
}
],
"maxArea": 17550000,
"extraFeatures": ["regionByPct", "regionByPx", "sizeByW", "sizeByH", "rotationBy90s", "..."]
}
The key fields are:
widthandheight— the full dimensions of the image in pixelsqualities— the image qualities available for this image. In this example:default,gray, andcolorformats— the output formats available for this image. In this example:jpg,png, andwebptiles— describes how the image is divided into tiles for use in deep zoom viewers such as OpenSeadragon or Universal Viewer. ThescaleFactorsindicate which zoom levels are available, andwidthandheightdefine the tile size. This is relevant if you want to integrate the image into a viewer rather than downloading it directly.maxArea— the maximum number of pixels that can be requested in a single image requestextraFeatures— the IIIF Image API features supported by this image. These indicate which transformations are supported for this image. For example,regionByPctmeans you can specify a region as a percentage of the image,sizeByWmeans you can request a specific width, androtationBy90smeans rotation is supported in steps of 90 degrees. A full overview of supported features is available in the IIIF Image API specification.
Step 3 — Retrieve an image
Every Image API request is a structured URL composed of 5 parts:
https://iiif.micr.io/{identifier}/{region}/{size}/{rotation}/{quality}.{format}
| Parameter | Description |
|---|---|
identifier | The image identifier, e.g. PJEZO |
region | The region of the image to retrieve (see below) |
size | the size of the output image (see below) |
rotation | Rotation in degrees, e.g. 0 for no rotation. Supported values depend on the extraFeatures of the image |
quality | Image quality. Available qualities are listed in info.json, e.g. default or gray |
format | File format. Available formats are listed in info.json, e.g. jpg or png |
Region options
full— the entire imagex,y,width,height— a crop in pixels, e.g.6000,5400,2000,1600crops a 2000×1600 pixel area starting at position 6000, 5400.pct:x,y,width,height— a crop as percentages, e.g. pct:41,44,14,13square— the largest square region centered on the image
Size options
max— maximum available size, limited bymaxAreaininfo.json800,— width of 800px, height scaled proportionally,600— height of 600px, width scaled proportionally800,600— forces exact width and height (may distort the image)pct:50— scale to 50% of the original size
Some examples:
| URL | Description |
|---|---|
| https://iiif.micr.io/PJEZO/full/800,/0/default.jpg | Full image resized to 800px wide |
| https://iiif.micr.io/PJEZO/square/pct:10/0/gray.jpg | Largest square region, scaled to 10%, in grayscale |
| https://iiif.micr.io/PJEZO/6000,5400,2000,1600/,600/0/default.png | Crop of the central figure's head, resized to 600px high in PNG format |
| https://iiif.micr.io/PJEZO/pct:41,44,14,13/,600/90/default.jpg | Same crop as percentages, resized to 600px high and rotated 90° in JPG format |
Step 4 — Retrieve a manifest
The IIIF Presentation API provides a manifest for each image. A manifest describes how a digital object is structured and how it should be displayed in a viewer.
To retrieve a manifest, append /manifest to the image endpoint:
https://iiif.micr.io/PJEZO/manifest
A simplified version of the response looks like this:
{
"@context": "http://iiif.io/api/presentation/3/context.json",
"id": "https://iiif.micr.io/PJEZO/manifest",
"type": "Manifest",
"label": { "en": ["95f9a7dcf194456dbdf70a2167fbb725"] },
"thumbnail": [
{
"id": "https://iiif.micr.io/PJEZO/full/80,100/0/default.jpg",
"type": "Image",
"format": "image/jpeg"
}
],
"provider": [
{
"type": "Agent",
"label": { "en": ["Rijks Collectie"] },
"id": "https://www.rijksmuseum.nl"
}
],
"items": [
{
"id": "https://iiif.micr.io/PJEZO/canvas",
"type": "Canvas",
"width": 14645,
"height": 12158,
"items": [
{
"id": "https://iiif.micr.io/PJEZO/annotation-page",
"type": "AnnotationPage",
"items": [
{
"id": "https://iiif.micr.io/PJEZO/annotation",
"type": "Annotation",
"motivation": "painting",
"body": {
"id": "https://iiif.micr.io/PJEZO/full/1024,/0/default.jpg",
"type": "Image",
"format": "image/jpeg",
"width": 14645,
"height": 12158,
"service": [
{
"id": "https://iiif.micr.io/PJEZO",
"type": "ImageService3",
"profile": "level2",
"qualities": ["default", "gray", "color"],
"formats": ["jpg", "png", "webp"],
"tiles": [{ "scaleFactors": [1, 2, 4, 8, 16], "width": 1024, "height": 1024 }],
"width": 14645,
"height": 12158,
"maxArea": 17550000,
"extraFeatures": ["regionByPct", "regionByPx", "sizeByW", "..."]
}
]
},
"target": "https://iiif.micr.io/PJEZO/canvas"
}
]
}
]
}
]
}
The key fields are:
label— an internal identifier for the image, not the artwork title. The title is available in the Linked Art record of the object.thumbnail— a small preview image, useful for displaying in lists or grids.provider— information about the organisation that provides the image.items— contains the canvas with the image. The canvas is the virtual space on which the image is displayed. It contains a reference to the image itself, including all the information you would also find ininfo.json— dimensions, qualities, formats, tiles, and supported features. This means a single manifest request gives you everything you need to use the Image API.
The manifest can be opened directly in a IIIF-compatible viewer such as Universal Viewer. This allows you to browse and zoom into the image interactively:
https://uv-v4.netlify.app/#?manifest=https://iiif.micr.io/PJEZO/manifest
To view a different image, replace PJEZO with the identifier of the image you want to display.
Note that the manifest does not contain descriptive metadata such as title or creator. To retrieve this information, resolve the object URI using the Linked Art API.
Step 5 — Change Discovery API
The Change Discovery API allows you to discover which resources were created, updated, or deleted over time, without needing to reharvest the complete dataset.
The API follows the IIIF Change Discovery specification and exposes an ordered stream of change events. It tracks changes to published resources and their metadata representations, such as EDM, Dublin Core and Linked Art serializations.
An important thing to understand upfront: the API is a signal mechanism, not a content delivery mechanism. Each event tells you that something changed, not what exactly changed.
A typical workflow therefore looks like this:
- Receive a change event indicating that a resource was created, updated, or deleted
- Re-fetch the current state of that resource from the appropriate endpoint
- Optionally compare with your local copy and update accordingly
The API exposes two URL patterns:
/{scope}/collection.json— the root entry point for a given scope/{scope}/pages/{page_token}.json— individual pages of change events, identified by an opaque page token
Start at the root collection:
https://data.rijksmuseum.nl/cd/collection.json
This returns an OrderedCollection pointing to the most recent page:
{
"@context": "http://iiif.io/api/discovery/1/context.json",
"@type": "OrderedCollection",
"id": "https://data.rijksmuseum.nl/cd/collection.json",
"last": {
"id": "https://data.rijksmuseum.nl/cd/pages/last.json",
"type": "OrderedCollectionPage"
}
}
The last page contains the most recent change events. A simplified example:
{
"orderedItems": [
{
"type": "Update",
"object": {
"id": "https://data.rijksmuseum.nl/200244439?_profile=edm",
"canonical": "https://data.rijksmuseum.nl/200244439",
"type": "Manifest",
"format": "application/ld+json",
"profile": "https://data.rijksmuseum.nl/schema/edm"
},
"endTime": "2026-05-07T12:40:23.045849+00:00"
}
],
"prev": {
"id": "https://data.rijksmuseum.nl/cd/pages/eyJ...",
"type": "OrderedCollectionPage"
}
}
The key fields are:
type— type of change:Create,Update, orDeleteobject.id— identifier of the specific representation that changed, including the metadata representation (e.g.?_profile=edm)object.canonical— base identifier of the underlying resource, independent of the metadata representationobject.format— content type of the representation (e.g. application/ld+json)object.profile— URI identifying the specific metadata schemaendTime— timestamp of the change eventprev— link to the previous page of change events
The API is designed to be consumed starting from the most recent page and working backwards through the prev links. Each page contains exactly 100 events, which is worth keeping in mind when estimating harvest volume.
A single object update typically produces multiple events — one for each metadata representation (EDM, Dublin Core, Linked Art, etc.). These events all refer to different metadata representations of the same underlying object.
For synchronization workflows, it is therefore recommended to group events by object.canonical to avoid processing the same object multiple times.
When re-fetching a changed resource, you can request a specific metadata representation using the ?_profile= parameter, for example:
?_profile=edm?_profile=dc?_profile=la-framed
Step 6 — Python examples
Image API
Basic example — retrieving the IIIF identifier
Before running this script, make sure the required library is installed:
pip install requests
The example below retrieves the IIIF image identifier for a given object by following the three-step chain described in Step 1. The script uses a fixed URI. Replace it with your own object URI.
import requests
def get_iiif_identifier(object_uri):
# Step 1: Resolve the object record and find the object number and VisualItem
response = requests.get(f"{object_uri}?_profile=la-framed")
record = response.json()
# Extract object number
object_number = None
for item in record.get('identified_by', []):
if item.get('type') == 'Identifier':
for classification in item.get('classified_as', []):
if classification.get('id') == 'https://id.rijksmuseum.nl/22015218':
object_number = item.get('content')
break
# Find the VisualItem
visual_item_uri = None
for item in record.get('shows', []):
visual_item_uri = item.get('id')
break
if not visual_item_uri:
print(f"No VisualItem found for {object_uri}")
return None
# Step 2: Resolve the VisualItem and find the DigitalObject
response = requests.get(f"{visual_item_uri.replace('id.rijksmuseum.nl', 'data.rijksmuseum.nl')}?_profile=la-framed")
visual_item = response.json()
digital_object_uri = None
for item in visual_item.get('digitally_shown_by', []):
digital_object_uri = item.get('id')
break
if not digital_object_uri:
print(f"No DigitalObject found for {visual_item_uri}")
return None
# Step 3: Resolve the DigitalObject and find the IIIF image URL
response = requests.get(f"{digital_object_uri.replace('id.rijksmuseum.nl', 'data.rijksmuseum.nl')}?_profile=la-framed")
digital_object = response.json()
for item in digital_object.get('access_point', []):
iiif_url = item.get('id')
if iiif_url and 'iiif.micr.io' in iiif_url:
identifier = iiif_url.split('iiif.micr.io/')[1].split('/')[0]
return object_number, identifier
print(f"No IIIF identifier found for {digital_object_uri}")
return None
# Example: retrieve the IIIF identifier for the Night Watch
object_uri = "https://data.rijksmuseum.nl/200107928"
result = get_iiif_identifier(object_uri)
if result:
object_number, identifier = result
print(f"Object number: {object_number}")
print(f"URI: {object_uri}")
print(f"IIIF identifier: {identifier}")
print(f"Image URL: https://iiif.micr.io/{identifier}/full/max/0/default.jpg")
This will produce the following output:
Object number: SK-C-5
URI: https://data.rijksmuseum.nl/200107928
IIIF identifier: PJEZO
Image URL: https://iiif.micr.io/PJEZO/full/max/0/default.jpg
Note: retrieving the identifier requires three separate requests. For large result sets, this can significantly impact performance.
Looking up an image by object number
Rather than hardcoding a URI, this example prompts you for an object number and looks up the corresponding URI automatically.
import requests
def get_uri_from_object_number(object_number):
# Look up the object by object number via the Search API
url = "https://data.rijksmuseum.nl/search/collection"
params = {"objectNumber": object_number}
response = requests.get(url, params=params)
data = response.json()
items = data.get('orderedItems', [])
if not items:
print(f"No object found for {object_number}")
return None
# Convert id.rijksmuseum.nl to data.rijksmuseum.nl
return items[0]['id'].replace('id.rijksmuseum.nl', 'data.rijksmuseum.nl')
def get_iiif_identifier(object_uri):
# Step 1: Resolve the object record and find the object number and VisualItem
response = requests.get(f"{object_uri}?_profile=la-framed")
record = response.json()
object_number = None
for item in record.get('identified_by', []):
if item.get('type') == 'Identifier':
for classification in item.get('classified_as', []):
if classification.get('id') == 'https://id.rijksmuseum.nl/22015218':
object_number = item.get('content')
break
visual_item_uri = None
for item in record.get('shows', []):
visual_item_uri = item.get('id')
break
if not visual_item_uri:
print(f"No VisualItem found for {object_uri}")
return None
# Step 2: Resolve the VisualItem and find the DigitalObject
response = requests.get(f"{visual_item_uri.replace('id.rijksmuseum.nl', 'data.rijksmuseum.nl')}?_profile=la-framed")
visual_item = response.json()
digital_object_uri = None
for item in visual_item.get('digitally_shown_by', []):
digital_object_uri = item.get('id')
break
if not digital_object_uri:
print(f"No DigitalObject found for {visual_item_uri}")
return None
# Step 3: Resolve the DigitalObject and find the IIIF image URL
response = requests.get(f"{digital_object_uri.replace('id.rijksmuseum.nl', 'data.rijksmuseum.nl')}?_profile=la-framed")
digital_object = response.json()
for item in digital_object.get('access_point', []):
iiif_url = item.get('id')
if iiif_url and 'iiif.micr.io' in iiif_url:
identifier = iiif_url.split('iiif.micr.io/')[1].split('/')[0]
return object_number, identifier
print(f"No IIIF identifier found for {digital_object_uri}")
return None
# Ask the user for an object number
object_number = input("Enter object number: ")
uri = get_uri_from_object_number(object_number)
if uri:
result = get_iiif_identifier(uri)
if result:
object_number, identifier = result
print(f"Object number: {object_number}")
print(f"URI: {uri}")
print(f"IIIF identifier: {identifier}")
print(f"Image URL: https://iiif.micr.io/{identifier}/full/max/0/default.jpg")
The output is identical to the previous example.
Note: this script sends four separate requests — one to the Search API and three to resolve the identifier chain. For large result sets, this can significantly impact performance.
Retrieving an image
The example below uses the IIIF identifier retrieved from the 'Night Watch' record to download the full image and save it to disk.
import requests
# Define the image identifier and construct the URL
identifier = "PJEZO"
url = f"https://iiif.micr.io/{identifier}/full/max/0/default.jpg"
# Download the image
response = requests.get(url)
# Save the image to disk
with open(f"{identifier}.jpg", "wb") as f:
f.write(response.content)
print(f"Image saved as {identifier}.jpg")
Applying transformations
The IIIF Image API allows you to retrieve a modified version of an image by changing the parameters in the URL. The example below retrieves a resized version of the Night Watch at 800 pixels wide.
import requests
# Define the image identifier and transformation parameters
identifier = "PJEZO"
region = "full" # full image, or e.g. "0,0,500,500" for a crop
size = "800," # width of 800px, height scaled proportionally
rotation = "0" # no rotation
quality = "default" # default quality
format = "jpg"
# Construct the URL
url = f"https://iiif.micr.io/{identifier}/{region}/{size}/{rotation}/{quality}.{format}"
# Download the image
response = requests.get(url)
# Save the image to disk
# This will be written to the current working directory
output_file = f"{identifier}_transformed.jpg"
with open(output_file, "wb") as f:
f.write(response.content)
print(f"Image saved as {output_file}")
The image is saved in the current working directory — the folder where the script is executed.
Change discovery API
Basic example — reading change events
The example below reads the most recent change events from the Change Discovery API and prints them.
import requests
# Start at the root collection
url = "https://data.rijksmuseum.nl/cd/collection.json"
response = requests.get(url)
data = response.json()
# Navigate to the last page
last_page_url = data['last']['id']
response = requests.get(last_page_url)
page = response.json()
# Print the most recent change events
for event in page['orderedItems']:
print(f"{event['endTime']} {event['type']} {event['object']['id']}")
This will produce output like:
2026-05-08T09:32:09.300578+00:00 Update https://data.rijksmuseum.nl/200889140?_profile=la-framed
2026-05-08T09:32:34.228897+00:00 Update https://data.rijksmuseum.nl/20026551?_profile=oai_dc
2026-05-08T09:32:34.319668+00:00 Update https://data.rijksmuseum.nl/20226551?_profile=la
2026-05-08T09:32:34.402865+00:00 Update https://data.rijksmuseum.nl/20026551?_profile=edm
2026-05-08T09:33:36.535200+00:00 Update https://data.rijksmuseum.nl/200889141?_profile=la
Notice that the same object (20026551) appears multiple times with different profiles. This illustrates that a single resource update produces multiple events - one per metadata representation.
Summarizing changes over the last 7 days
The example below walks backwards through the change stream and collects all created, updated, and deleted objects within the last 7 days, grouped by canonical identifier. Deleted resources are printed as they are encountered.
import requests
from datetime import datetime, timedelta, timezone
# Define the lookback period
cutoff = datetime.now(timezone.utc) - timedelta(days=7)
# Collect unique canonical identifiers per change type
created = set()
updated = set()
deleted = set()
# Start at the root collection
data = requests.get("https://data.rijksmuseum.nl/cd/collection.json").json()
page_url = data["last"]["id"]
pages_processed = 0
# Walk backwards through the change stream
while page_url:
pages_processed += 1
page = requests.get(page_url).json()
events = page.get("orderedItems", [])
if not events:
break
stop = False
for event in events:
event_time = datetime.fromisoformat(event["endTime"])
# Stop when we reach events older than the cutoff
if event_time < cutoff:
stop = True
break
canonical = event["object"]["canonical"]
if event["type"] == "Create":
created.add(canonical)
elif event["type"] == "Update":
updated.add(canonical)
elif event["type"] == "Delete":
if canonical not in deleted:
print(f"Deleted: {canonical} at {event['endTime']}")
deleted.add(canonical)
if stop:
break
# Move to the previous page
page_url = page.get("prev", {}).get("id")
# Print summary
print(f"\nChanges in the last 7 days ({pages_processed} pages processed)\n")
print(f"Created: {len(created)}")
print(f"Updated: {len(updated)}")
print(f"Deleted: {len(deleted)}")
This will produce output like:
Deleted: https://data.rijksmuseum.nl/200913727 at 2026-05-08T00:51:26.809292+00:00
Deleted: https://data.rijksmuseum.nl/202913727 at 2026-05-08T00:51:27.163327+00:00
// ... more deleted resources ...
Changes in the last 7 days (227 pages processed)
Created: 1839
Updated: 4839
Deleted: 39
Note: canonical identifiers in the change stream may refer to different kinds of published resources, not only collection objects. The counts above therefore reflect changed resources, not necessarily changed collection objects. Whether to remove, archive, or ignore deleted resources depends on your use case.
Summary
In this tutorial, you learned how to work with the three Rijksmuseum IIIF APIs.
You learned how to retrieve an IIIF image identifier by following the three-step chain from object record to VisualItem to DigitalObject, and how to automate this process using Python.
You explored the Image API URL syntax and how to apply transformations such as cropping, resizing, rotation, and format conversion.
You also learned how to retrieve a manifest using the Presentation API and how to open it in a IIIF-compatible viewer.
Finally, you learned how the Change Discovery API works as a signal mechanism for tracking changes to published resources over time, and how to use it in practice to collect and summarize created, updated, and deleted resources.
For more information, see the IIIF Image API 3.0 specification, the IIIF Presentation API 3.0 specification, the IIIF Change Discovery specification, and the Micrio documentation.