Page scans API
Scan a web page for every image it loads, compress them all, and download them as one zip. Every page scans endpoint with samples.
A page scan opens a web page in a real browser on Iminify's servers, waits for it to load, scrolls to the bottom so lazy images load too, and collects every image the page loaded along the way. Each one is then downloaded and compressed with the settings you gave the scan, as an image of its own on your account, and the scan reports what's worth fixing in how the page shows them.
It's the same scanner as the website's. A scan counts one against your daily page scans; the images it finds don't count against your daily images.
To scan several pages of one site at once, on a paid plan, start a site crawl: each of its pages is a page scan, named by the crawl's id in crawl_id. The scan list leaves a crawl's pages out, because the crawl lists them, and a page is deleted with its crawl, never on its own.
The life of a scan
queued ──► scanning ──► completed ──► (its images compress)
│ ├──► no-image-found
│ └──► failed
└──► cancelled
The scanner itself takes from a few seconds to a few minutes, depending on the page. When it's done the scan is completed and its images are queued, and they keep compressing after that. The scan is fully done once its status has ended and images_in_progress is 0. note says what the scanner found and what it did with each image, or why it failed.
A scan passes over SVG and AVIF files, images larger than your plan's file size, tiny tracking pixels, and the same image found twice. A page of a weekly site check also passes over an image whose file an earlier check compressed, as unchanged. Each one it passed over is listed in skipped with its address, a reason code and a message, so you can tell which file it was. It stops at your plan's images per scan; the images past that were never fetched, so note counts them but skipped doesn't list them. A page on a private network is refused, and a page behind a login usually comes back with no images, because the scanner sees what a signed-out visitor sees.
The page report
A scan also reports how the page shows its images. The same browser measures them at a desktop width of 1366 pixels, before anything has scrolled. Get a single scan and the report is in audit (the ScanAudit object); on the website, it opens from the scan's Report button. It covers every image address the page had, including the ones past your plan's images per scan: the browser measured those, even though the scan didn't download them. audit.images lists up to 1000 addresses, and images_found counts them all.
Each entry in audit.images has the image's format and bytes as a browser received them, its own size against the size it's shown at, whether it's in the first screen, its alt text, and a list of findings:
| Finding | What it means | The fix |
|---|---|---|
oversized |
The file is more than twice as wide and twice as tall as the space it's shown in. Twice is what a high density screen draws, so an image sized for those isn't flagged. Only images an img element shows are sized; a CSS background isn't. |
Resize it to twice its shown size. |
without_dimensions |
The image has no width and height attributes and no CSS aspect ratio, so the page moves when it arrives. Images placed with position: absolute or fixed don't count. |
Add both attributes. |
lazy_above_fold |
loading="lazy" on an image in the first screen, which delays it. |
Remove the attribute. |
eager_below_fold |
An image below the first screen that loads straight away. An image a script loads as you scroll doesn't count. | Add loading="lazy". |
missing_alt |
The image has no alt text. alt="", which marks an image as decorative, is fine. |
Describe the image (suggested_alt has a start), or use alt="". |
lcp_not_prioritized |
The image is the page's largest contentful paint (largest_paint) and lacks fetchpriority="high". |
Add the attribute. |
wasted_bytes on an oversized image, and oversized_bytes for the whole page, are estimates. They take the share of the image's pixels that would go at twice its shown size and apply it to the image's bytes. Bytes don't shrink exactly with pixels, so read them as "about".
largest_paint names the page's largest contentful paint: an image (with its address), text, or none. A browser asks for WebP and AVIF and the scan's own download doesn't, so a site can send the two different files: format and bytes are the browser's, and the compressed image (image_id) has its own sizes. When the scan's format is auto, format_sizes shows what the compressed file came to in each format it tried.
For an image with missing_alt, the scan writes an AI alt text from its compressed copy and the report carries it in suggested_alt. They're written one at a time after the images finish, so the scan is done for the report once alt_texts_in_progress is 0 as well. Images marked decorative, or that already have alt text, get none, and nothing renames a scan's images. An image whose bytes the scan already compressed under an address that has alt text shares that copy and gets none; ask for one on its image_id and the report shows it. Send alt_text as false with the scan to leave it out, or alt_text_language for another language; each one counts against your daily AI alt texts.
outcome says what the scan did with each address: compressed, skipped (with a skip_reason), or left_out when it never got to it. An address that redirected is listed once, under the address the page asked for, with the format and bytes of the final answer. If the browser can't open a page, the scan reads its HTML instead; then rendered is false and only the addresses are known. audit is null until the scan has finished, for a scan that failed, and for scans made before the report existed.
Sharing and exporting the report
On a paid plan, a finished scan's report can be handed to someone without an account, three ways. Other plans are answered with pro_required, and a scan without a report with report_not_available.
- A public link.
POST /scans/{id}/sharegives the report a page at an address nobody can guess and returns the scan with it inshare_url. Anyone with the link can read the report, with thumbnails of the compressed images; nothing on the page can be downloaded or changed, and search engines are asked not to index it. Sharing again keeps the same link, andDELETE /scans/{id}/shareswitches it off. The link answers only while your account is on a paid plan: it pauses if the plan ends and comes back on renewal, and you can switch it off on any plan. - A CSV.
GET /scans/{id}/report.csvhas one row per image address, with the fieldsaudit.imageshas,findingsseparated by spaces, the bytes of the file the scan downloaded, and the compressed copy's format and bytes. A page that could only be read as HTML leaves its flags empty. A cell that would start with=,+,-or@starts with an apostrophe, so a spreadsheet never runs it as a formula. - A PDF.
GET /scans/{id}/report.pdfprints the report for a client: the findings with their fixes, the page's largest paint and formats, then every image to fix. The CSV lists every image. Printing takes a second or two, and one PDF prints at a time on your account; a second request meanwhile is answered withrate_limited.
Both files are made for the request that asks for them and are never stored. On the website, the three are buttons in the scan's report; through MCP they are share_scan_report and export_scan_report.
A whole page in one script
Scan a page, wait for the scanner and every image, then download them all as one zip:
#!/usr/bin/env bash
# Scan a page, wait for the scanner and every image it found, save them as one zip.
# Needs curl 7.76 or newer and jq.
# Each call gives up after 10 seconds without a connection and after --max-time seconds overall,
# so a stalled network can't hang the script.
set -euo pipefail
API="https://www.iminify.com/api/v1"
AUTH="Authorization: Bearer $IMINIFY_API_KEY"
# 1. Queue the scan with the settings every image gets.
id=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 30 "$API/scans" \
-H "$AUTH" -H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/pricing", "format": "webp"}' \
| jq -r '.data.id')
# 2. Wait for the scanner, then for the images it queued.
while true; do
scan=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 30 "$API/scans/$id" \
-H "$AUTH" -H "Accept: application/json")
status=$(jq -r '.data.status' <<< "$scan")
pending=$(jq -r '.data.images_in_progress' <<< "$scan")
case "$status" in
queued|scanning) ;;
completed) [ "$pending" -eq 0 ] && break ;;
*) jq -r '.data.note' <<< "$scan" >&2; exit 1 ;;
esac
sleep 5
done
jq -r '.data | "\(.images_finished) images, \(.saved_percent)% smaller"' <<< "$scan"
# 3. Download every finished image as one zip.
curl -sS --fail-with-body --connect-timeout 10 --max-time 1800 -o scan.zip "$API/scans/$id/download" -H "$AUTH"
"""Scan a page, wait for the scanner and every image it found, save them as one zip.
Needs Python 3.8 or newer and the requests package (pip install requests).
"""
import os
import time
import requests
API = "https://www.iminify.com/api/v1"
# requests waits forever by default. These calls give up after 10 seconds without
# a connection and after 30 seconds without an answer, or 5 minutes for the zip.
TIMEOUT = (10, 30)
FILE_TIMEOUT = (10, 300)
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
})
def running(scan):
return scan["status"] in ("queued", "scanning") or (scan["status"] == "completed" and scan["images_in_progress"] > 0)
# 1. Queue the scan with the settings every image gets.
response = session.post(f"{API}/scans", json={"url": "https://example.com/pricing", "format": "webp"}, timeout=TIMEOUT)
response.raise_for_status()
scan = response.json()["data"]
# 2. Wait for the scanner, then for the images it queued.
while running(scan):
time.sleep(5)
response = session.get(f"{API}/scans/{scan['id']}", timeout=TIMEOUT)
response.raise_for_status()
scan = response.json()["data"]
if scan["status"] != "completed":
raise SystemExit(scan["note"] or f"The scan ended as {scan['status']}")
print(f"{scan['images_finished']} images, {scan['saved_percent']}% smaller")
# 3. Download every finished image as one zip.
response = session.get(scan["download_url"], timeout=FILE_TIMEOUT)
response.raise_for_status()
with open("scan.zip", "wb") as out:
out.write(response.content)
<?php
declare(strict_types=1);
// Scan a page, wait for the scanner and every image it found, save them as one zip.
// Needs PHP 8.1 or newer and Guzzle (composer require guzzlehttp/guzzle).
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
// Guzzle waits forever by default. These calls give up after 10 seconds without a
// connection and after 30 seconds overall, or 30 minutes for the zip, so a large one
// gets through a slow connection.
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$running = fn (array $scan): bool => in_array($scan['status'], ['queued', 'scanning'], true)
|| ('completed' === $scan['status'] && $scan['images_in_progress'] > 0);
// 1. Queue the scan with the settings every image gets.
$response = $client->post('scans', [
'json' => ['url' => 'https://example.com/pricing', 'format' => 'webp'],
]);
$scan = json_decode((string) $response->getBody(), true)['data'];
// 2. Wait for the scanner, then for the images it queued.
while ($running($scan)) {
sleep(5);
$response = $client->get("scans/{$scan['id']}");
$scan = json_decode((string) $response->getBody(), true)['data'];
}
if ('completed' !== $scan['status']) {
fwrite(STDERR, ($scan['note'] ?? "The scan ended as {$scan['status']}") . "\n");
exit(1);
}
echo "{$scan['images_finished']} images, {$scan['saved_percent']}% smaller\n";
// 3. Download every finished image as one zip.
$client->get("scans/{$scan['id']}/download", ['sink' => 'scan.zip', 'timeout' => 1800]);
// Scan a page, wait for the scanner and every image it found, save them as one zip.
// Needs Node 20 or newer. Save it as scan.mjs and run: node scan.mjs
import { writeFile } from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';
const API = 'https://www.iminify.com/api/v1';
// Node's fetch waits up to five minutes on a stalled connection. Each call gives up
// after `seconds` instead: 30, or 1800 for the zip, so a large one gets through a
// slow connection.
const call = async (path, options = {}, seconds = 30) => {
const response = await fetch(`${API}${path}`, {
...options,
signal: AbortSignal.timeout(seconds * 1000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
return response;
};
const running = (scan) =>
['queued', 'scanning'].includes(scan.status) || (scan.status === 'completed' && scan.images_in_progress > 0);
// 1. Queue the scan with the settings every image gets.
const queued = await call('/scans', {
method: 'POST',
body: JSON.stringify({ url: 'https://example.com/pricing', format: 'webp' }),
});
let { data: scan } = await queued.json();
// 2. Wait for the scanner, then for the images it queued.
while (running(scan)) {
await sleep(5000);
({ data: scan } = await (await call(`/scans/${scan.id}`)).json());
}
if (scan.status !== 'completed') {
throw new Error(scan.note ?? `The scan ended as ${scan.status}`);
}
console.log(`${scan.images_finished} images, ${scan.saved_percent}% smaller`);
// 3. Download every finished image as one zip.
const download = await call(`/scans/${scan.id}/download`, {}, 1800);
await writeFile('scan.zip', Buffer.from(await download.arrayBuffer()));
What the zip holds
A scan's zip is laid out like the page. Each image sits in the folders of its address, so https://example.com/wp-content/uploads/2024/hero.jpg comes back as wp-content/uploads/2024/hero.jpg. An image from another host, a CDN say, sits under a folder named after that host (cdn.example.net/...). The query string is dropped.
An image keeps its file name while its format stays the same, so it can replace the original as it is. A converted image keeps the name and takes its new extension (hero.webp), and a renamed one takes the name you gave it. Two images that land on the same path get (2) before the extension of the second.
manifest.csv at the root has a row for every image address the scan fetched: the address, the file in the zip, the status, both formats and both sizes. Addresses it passed over are listed with their reason and message, and a duplicate points at the file it shares with the first copy. Images past your plan's images per scan were never fetched, so a last row only counts them. Each image's address is also its source_url. Images from scans made before addresses were kept have none, so they sit at the root.
If you compress one of the scan's images again, both results go in the zip. The newer one takes the page's file name and the older one gets the (2).
The Scan object
-
idstring - The scan's id.
-
urlstring - The page that was scanned.
-
crawl_idstring or null -
The site crawl this scan is a page of, or
nullfor a page scanned on its own. A crawl's pages are left out of the scan list, because the crawl lists them, and are deleted with their crawl. -
statusstring -
Where the scanner is.
queued: waiting for the scanner.scanning: the page is open and its images are being collected.completed: images were found and queued.no-image-found: the page loaded but nothing on it could be compressed.failed: the page could not be scanned.cancelled: called off before it started. The images themselves keep compressing aftercompleted; watchimages_in_progress. -
notestring or null -
What the scanner wrote when it finished: how many images it found and what became of each, or why it failed.
nulluntil then. -
settingsobject - The settings every image the scan found is compressed with.
-
settings.levelstring - The compression level.
-
settings.formatstring or null -
The format every image is converted to, or
nullto keep each one's own. -
settings.keep_metadataboolean - Whether EXIF metadata is kept.
-
settings.widthinteger or null -
The width every image is resized to, or
null. -
settings.heightinteger or null -
The height every image is resized to, or
null. -
settings.scaleinteger or null -
The percentage every image is scaled to, or
null. -
skippedarray -
Every image the scan found and did not compress, with why:
duplicate(the same bytes as another address on the page, which was tried instead, or on a page of a site crawl an image another page of the crawl compressed first, whichmessagenames),unsupported_format(an SVG, say),too_large,tracking_pixel,private_network,unreadable,unreachable(the address failed to answer, or answered with an error),refused(it failed the checks every upload goes through) orfailed.messagesays the same in a sentence. Images past your plan's per-scan limit or the scan's time budget were never fetched, so they are not listed;notecounts them. Empty until the scan finishes. -
images_countinteger - How many images the scan queued.
-
images_in_progressinteger - How many of them are still queued or being compressed. The scan is done once its status has ended and this is 0.
-
alt_texts_in_progressinteger -
How many finished images still have an AI alt text being written for the report's
suggested_alt. They are written one at a time, so this can take a minute afterimages_in_progressreaches 0. -
images_finishedinteger - How many of them have finished.
-
original_sizeinteger - Bytes of the finished images before compression.
-
optimized_sizeinteger - Bytes of the same images after compression.
-
saved_bytesinteger - The difference between the two.
-
saved_percentnumber or null -
The same saving as a percentage, or
nullwhile no image has finished. -
download_urlstring - Where to download every finished image as one zip.
-
imagesarray - The scan's images, newest first. Only on a single scan, not in a list.
-
auditstring or null -
The page audit: how the page shows its images (oversized files, missing dimensions, lazy loading in the wrong place, missing alt text, the largest contentful paint), measured in a browser for every image the page had, including the ones past your plan's images per scan.
nulluntil the scan has finished, for a scan that failed, and for one made before the audit existed. Only on a single scan, not in a list. -
share_urlstring or null -
The page report's public link while it is shared, or
null. The link answers only while your account is on a paid plan. -
created_attimestamp - When the scan was queued.
-
updated_attimestamp - When it last changed.
The ScanAudit object
-
renderedboolean -
Whether a browser opened the page.
falsewhen only its HTML could be read: then the image addresses are known and nothing about how they are shown, so every finding count is 0. -
images_foundinteger -
How many image addresses the page had.
imageslists them all unless a page had more than 1000. -
largest_paintobject or null -
The page's largest contentful paint (LCP): the biggest thing in the first screen, which is when a visitor sees the page as loaded.
nullwhen it could not be measured. -
largest_paint.kindstring -
largest_paint.elementstring or null -
largest_paint.urlstring or null -
findingsobject - How many images have each finding.
-
findings.oversizedinteger - Images whose file is more than twice as wide and twice as tall as the space they are shown in.
-
findings.without_dimensionsinteger - Images shown without width and height attributes (and no CSS aspect ratio), so the page can move as they load.
-
findings.lazy_above_foldinteger - Images in the first screen that are lazy loaded, which delays them.
-
findings.eager_below_foldinteger - Images below the first screen that are not lazy loaded, so they load before they are needed.
-
findings.missing_altinteger -
Images without alt text. An empty
alt="", which marks an image as decorative, is not counted. -
findings.lcp_not_prioritizedinteger -
1 when the largest paint is an image without
fetchpriority="high", which would have the browser fetch it first. -
oversized_bytesinteger - About how many bytes the oversized images would shed if each were served at twice the size it is shown, the size a high density screen draws. An estimate from the share of pixels that would go.
-
bytesinteger - The bytes a browser downloaded for the images listed.
-
formatsarray - How many images arrived in each format, the commonest first, as a browser received them: a site can send a browser WebP or AVIF and the scan's own download the original format.
-
imagesarray - Every image address the page had, in the page's order.
The ScanAuditImage object
-
urlstring - The image's address on the page.
-
formatstring or null -
The format a browser received it in (
jpg,png,webp,avif,gif,svg, ...), ornullwhen it did not load as an image. -
bytesinteger or null -
The bytes a browser downloaded for it, or
nullwhen it did not load as an image. -
natural_widthinteger or null -
The file's own width in pixels, or
nullwhen it is not shown by animgelement or did not load. -
natural_heightinteger or null - The file's own height in pixels.
-
rendered_widthinteger or null -
The width it is shown at on a desktop screen 1366 pixels wide, the largest where the page shows it more than once.
nullwhen noimgelement shows it (a CSS background, a hidden image). -
rendered_heightinteger or null - The height it is shown at.
-
above_foldboolean or null -
Whether it is in the first screen before scrolling, or
nullwhen that is not known. -
largest_paintboolean - Whether it is the page's largest contentful paint.
-
altstring or null -
Whether it has alt text:
present,empty(alt="", or marked as decorative) ormissing.nullwhen noimgelement shows it. -
findingsarray - What is worth fixing about it, empty when nothing.
-
wasted_bytesinteger or null -
About how many bytes it would shed at twice the size it is shown, when it is oversized; otherwise
null. -
outcomestring -
What the scan did with it.
compressed: it is one of the scan's images (image_id).skipped: passed over, withskip_reason; aduplicatenames the image it shares its bytes with inimage_id, which on a page of a site crawl can be an image another page of the crawl compressed.left_out: never compressed, because the scan stopped at your plan's images per scan or its time budget, or the image has been deleted since. -
skip_reasonstring or null -
Why it was skipped, the same codes as the scan's
skippedlist, ornull. -
image_idstring or null -
The id of its compressed image, or
null. -
format_sizesarray or null -
What the compressed file came to in each format, smallest first, when the scan's format was
autoand more than one format was tried; otherwisenull. -
suggested_altstring or null -
An AI alt text for an image the page shows without one, written from its compressed copy, or
null: the image has alt text, the scan was asked for none, it is still being written (the scan'salt_texts_in_progresscounts it), the day's AI alt texts were used up, or the model gave no usable answer. See Alt text and file names.
List page scans
/api/v1/scans
Every page scan on your account, newest first, each with its counts and totals. The pages of a site crawl are left out: the crawl lists them. The images themselves come with a single scan, or from the image list filtered by scan.
Query parameters
-
statusstring -
Only scans with this status.
queuedscanningcompletedfailedno-image-foundcancelled -
per_pageinteger - Scans a page, 1 to 100. 1 to 100 Default: 15
-
pageinteger - The page to read. 1 to 10000000 Default: 1
Request
curl "https://www.iminify.com/api/v1/scans?status=completed" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.get(
f"{API}/scans",
headers=HEADERS,
timeout=(10, 30),
params={
"status": "completed",
},
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('scans', [
'query' => [
'status' => 'completed',
],
]);
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans?status=completed', {
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 200
{
"data": [
{
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "completed",
"note": "Rendered in a headless browser. 14 image URLs found: 12 queued for compression, 1 duplicates of an image already queued, 1 in a format the compressor does not accept.",
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [
{
"url": "https://cdn.example.com/pricing/hero.jpg?v=2",
"reason": "duplicate",
"message": "The same image as https://example.com/images/hero.jpg, so it was compressed once."
},
{
"url": "https://example.com/images/logo.svg",
"reason": "unsupported_format",
"message": "The file is SVG, a format Iminify does not compress."
}
],
"images_count": 12,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 12,
"original_size": 18406331,
"optimized_size": 2114902,
"saved_bytes": 16291429,
"saved_percent": 88.51,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"share_url": null,
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:41+00:00"
}
],
"links": {
"first": "https://www.iminify.com/api/v1/scans?page=1",
"last": "https://www.iminify.com/api/v1/scans?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [
{
"url": null,
"label": "« Previous",
"page": null,
"active": false
},
{
"url": "https://www.iminify.com/api/v1/scans?page=1",
"label": "1",
"page": 1,
"active": true
},
{
"url": null,
"label": "Next »",
"page": null,
"active": false
}
],
"path": "https://www.iminify.com/api/v1/scans",
"per_page": 15,
"to": 1,
"total": 1
}
}
Scan a page
/api/v1/scans
Opens the page in a browser on Iminify's servers, waits for it to load, scrolls to the bottom and collects every image it loaded, then compresses each one with the settings you send. It answers straight away with the status queued; a scan takes from a few seconds to a few minutes. Each scan counts one against your daily scans and your page scans a minute, and the images it finds don't count against your daily images. A scan stops at your plan's images per scan, skips SVG and AVIF files and anything over your plan's file size, and compresses an image that appears twice once.
The address can leave out https://. A page on a private network is refused. A page behind a login, or one that blocks automated browsers, usually comes back with no images, because the scanner sees what a signed-out visitor would.
Body (JSON)
-
urlstring (URL) required -
The page to scan.
https://can be left out. -
levelstring -
How hard to compress.
smartfinds the lowest quality that still looks like your upload,ultragoes further and softens fine texture a little,losslesschanges no pixel unless asize_limitneeds it to,noneonly applies the format, the resize, the size limit and the metadata choice. See Compression settings.
Default: smartnonelosslessultrasmart -
formatstring -
Convert to this format.
autoencodes the image in every format that can hold it and keeps the smallest. Leave it out to keep the upload's own format; a HEIC or TIFF always comes back in another one, because Iminify doesn't write either.jpegis read asjpg.autowebpavifpngjpg -
keep_metadataboolean - Keep the EXIF metadata (camera, date, location) in the optimized copy. Off by default, which strips it; the orientation is applied to the pixels first, so a photo never comes back on its side. Default: false
-
widthinteger -
Resize to this width in pixels. Alone, the height follows the aspect ratio. With
heighttoo, the image is resized to exactly that size, and its proportions change if they differ. Up to 65535. 1 to 65535 -
heightinteger -
Resize to this height in pixels, the same way as
width. Up to 65535. 1 to 65535 -
scaleinteger -
Resize to this percentage of the original, 1 to 100. Cannot be combined with
widthorheight. 1 to 100 -
alt_textboolean -
Write an AI alt text for each image the page shows with no alt attribute at all, from its compressed copy, for the report's
suggested_alt. On by default; sendfalseto leave it out. Images marked decorative (alt="") or that already have alt text get none. Each one counts against your daily AI alt texts. See Alt text and file names. Default: true -
alt_text_languagestring -
The language to write the alt text in, as one of these tags:
en(English),es(Spanish),fr(French),de(German),it(Italian),pt(Portuguese),nl(Dutch),pl(Polish),sv(Swedish),tr(Turkish),ru(Russian),uk(Ukrainian),ar(Arabic),fa(Persian),he(Hebrew),hi(Hindi),id(Indonesian),vi(Vietnamese),th(Thai),ja(Japanese),ko(Korean),zh-Hans(Simplified Chinese),zh-Hant(Traditional Chinese). The suggested file name follows it when the language is written in the Latin alphabet, and is in English otherwise.
Default: enenesfrdeitptnlplsvtrruukarfahehiidvithjakozh-Hanszh-Hant
Request
curl -X POST "https://www.iminify.com/api/v1/scans" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/pricing","level":"smart","format":"webp"}'
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.post(
f"{API}/scans",
headers=HEADERS,
timeout=(10, 30),
json={
"url": "https://example.com/pricing",
"level": "smart",
"format": "webp",
},
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->post('scans', [
'json' => [
'url' => 'https://example.com/pricing',
'level' => 'smart',
'format' => 'webp',
],
]);
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans', {
method: 'POST',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/pricing',
level: 'smart',
format: 'webp',
}),
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 202
{
"data": {
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "queued",
"note": null,
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [],
"images_count": 0,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 0,
"original_size": 0,
"optimized_size": 0,
"saved_bytes": 0,
"saved_percent": null,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"share_url": null,
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:00+00:00"
}
}
Get a page scan
/api/v1/scans/{id}
One scan with every image it found, newest first, and its page audit. Poll this while a scan runs: it is done once its status is no longer queued or scanning and images_in_progress is 0, and the report's suggested_alt texts once alt_texts_in_progress is 0 too.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.get(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
headers=HEADERS,
timeout=(10, 30),
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7');
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7', {
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 200
{
"data": {
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "completed",
"note": "Rendered in a headless browser. 14 image URLs found: 12 queued for compression, 1 duplicates of an image already queued, 1 in a format the compressor does not accept.",
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [
{
"url": "https://cdn.example.com/pricing/hero.jpg?v=2",
"reason": "duplicate",
"message": "The same image as https://example.com/images/hero.jpg, so it was compressed once."
},
{
"url": "https://example.com/images/logo.svg",
"reason": "unsupported_format",
"message": "The file is SVG, a format Iminify does not compress."
}
],
"images_count": 12,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 12,
"original_size": 18406331,
"optimized_size": 2114902,
"saved_bytes": 16291429,
"saved_percent": 88.51,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"images": [
{
"id": "9d3c5b8e-6f0a-4c1e-9b7d-2a4e8f1c6b35",
"status": "completed",
"name": "team-offsite.webp",
"original": {
"name": "team-offsite.jpg",
"format": "jpg",
"size": 2841205,
"width": 4032,
"height": 3024,
"download_url": "https://www.iminify.com/api/v1/images/9d3c5b8e-6f0a-4c1e-9b7d-2a4e8f1c6b35/download?type=original"
},
"optimized": {
"name": "team-offsite.webp",
"format": "webp",
"size": 212877,
"width": 1600,
"height": 1200,
"download_url": "https://www.iminify.com/api/v1/images/9d3c5b8e-6f0a-4c1e-9b7d-2a4e8f1c6b35/download"
},
"saved_bytes": 2628328,
"saved_percent": 92.51,
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null,
"size_limit": null,
"responsive_widths": null
},
"size_limit_status": null,
"content_credentials": {
"status": "removed",
"generative_ai": false
},
"alt_text": null,
"responsive": null,
"scan_id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"source_url": "https://example.com/images/team-offsite.jpg",
"folder": null,
"share_url": null,
"created_at": "2026-10-01T09:30:00+00:00",
"updated_at": "2026-10-01T09:30:09+00:00"
},
{
"id": "9d3c5b91-0b2e-4a7f-8c1d-5e6f7a8b9c0d",
"status": "completed",
"name": "hero-banner.webp",
"original": {
"name": "hero-banner.jpg",
"format": "jpg",
"size": 2841205,
"width": 4032,
"height": 3024,
"download_url": "https://www.iminify.com/api/v1/images/9d3c5b91-0b2e-4a7f-8c1d-5e6f7a8b9c0d/download?type=original"
},
"optimized": {
"name": "hero-banner.webp",
"format": "webp",
"size": 212877,
"width": 1600,
"height": 1200,
"download_url": "https://www.iminify.com/api/v1/images/9d3c5b91-0b2e-4a7f-8c1d-5e6f7a8b9c0d/download"
},
"saved_bytes": 2628328,
"saved_percent": 92.51,
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null,
"size_limit": null,
"responsive_widths": null
},
"size_limit_status": null,
"content_credentials": {
"status": "removed",
"generative_ai": false
},
"alt_text": null,
"responsive": null,
"scan_id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"source_url": "https://example.com/images/hero-banner.jpg",
"folder": null,
"share_url": null,
"created_at": "2026-10-01T09:30:00+00:00",
"updated_at": "2026-10-01T09:30:09+00:00"
}
],
"audit": {
"rendered": true,
"images_found": 14,
"largest_paint": {
"kind": "image",
"element": "img",
"url": "https://example.com/images/hero-banner.jpg"
},
"findings": {
"oversized": 1,
"without_dimensions": 1,
"lazy_above_fold": 0,
"eager_below_fold": 1,
"missing_alt": 1,
"lcp_not_prioritized": 1
},
"oversized_bytes": 180384,
"bytes": 899380,
"formats": [
{
"format": "webp",
"count": 2
},
{
"format": "jpg",
"count": 1
},
{
"format": "svg",
"count": 1
}
],
"images": [
{
"url": "https://example.com/images/hero-banner.jpg",
"format": "webp",
"bytes": 412306,
"natural_width": 3200,
"natural_height": 1800,
"rendered_width": 1200,
"rendered_height": 675,
"above_fold": true,
"largest_paint": true,
"alt": "present",
"findings": [
"oversized",
"lcp_not_prioritized"
],
"wasted_bytes": 180384,
"outcome": "compressed",
"skip_reason": null,
"image_id": "9d3c5b91-0b2e-4a7f-8c1d-5e6f7a8b9c0d",
"format_sizes": null,
"suggested_alt": null
},
{
"url": "https://example.com/images/team-offsite.jpg",
"format": "webp",
"bytes": 96118,
"natural_width": 1600,
"natural_height": 1200,
"rendered_width": 800,
"rendered_height": 600,
"above_fold": false,
"largest_paint": false,
"alt": "missing",
"findings": [
"without_dimensions",
"eager_below_fold",
"missing_alt"
],
"wasted_bytes": null,
"outcome": "compressed",
"skip_reason": null,
"image_id": "9d3c5b8e-6f0a-4c1e-9b7d-2a4e8f1c6b35",
"format_sizes": null,
"suggested_alt": null
},
{
"url": "https://cdn.example.com/pricing/hero.jpg?v=2",
"format": "jpg",
"bytes": 388042,
"natural_width": null,
"natural_height": null,
"rendered_width": null,
"rendered_height": null,
"above_fold": null,
"largest_paint": false,
"alt": null,
"findings": [],
"wasted_bytes": null,
"outcome": "skipped",
"skip_reason": "duplicate",
"image_id": null,
"format_sizes": null,
"suggested_alt": null
},
{
"url": "https://example.com/images/logo.svg",
"format": "svg",
"bytes": 2914,
"natural_width": 160,
"natural_height": 40,
"rendered_width": 160,
"rendered_height": 40,
"above_fold": true,
"largest_paint": false,
"alt": "present",
"findings": [],
"wasted_bytes": null,
"outcome": "skipped",
"skip_reason": "unsupported_format",
"image_id": null,
"format_sizes": null,
"suggested_alt": null
}
]
},
"share_url": null,
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:41+00:00"
}
}
Delete a page scan
/api/v1/scans/{id}
Deletes the scan and every image it produced, with their files, for good. A scan that is still running, or has an image still compressing, is refused, and so is a page of a site crawl, which goes with its crawl.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl -X DELETE "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7" \
--connect-timeout 10 --max-time 1800 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.delete(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
headers=HEADERS,
timeout=(10, 300),
)
response.raise_for_status()
print(response.status_code) # 204, nothing in the body
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->delete('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7', [
'timeout' => 1800,
]);
echo $response->getStatusCode(), PHP_EOL; // 204, nothing in the body
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7', {
method: 'DELETE',
signal: AbortSignal.timeout(1800_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(response.status); // 204, nothing in the body
Response 204
No content.
Retry a page scan
/api/v1/scans/{id}/retry
Queues a failed or cancelled scan again with the settings it had. It counts as a scan.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl -X POST "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/retry" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.post(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/retry",
headers=HEADERS,
timeout=(10, 30),
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->post('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/retry');
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/retry', {
method: 'POST',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 202
{
"data": {
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "queued",
"note": null,
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [],
"images_count": 0,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 0,
"original_size": 0,
"optimized_size": 0,
"saved_bytes": 0,
"saved_percent": null,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"share_url": null,
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:00+00:00"
}
}
Cancel a page scan
/api/v1/scans/{id}/cancel
Calls off a scan still waiting in the queue. Once the scanner has started, the scan runs to the end; its images can still be cancelled one by one.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl -X POST "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/cancel" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.post(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/cancel",
headers=HEADERS,
timeout=(10, 30),
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->post('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/cancel');
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/cancel', {
method: 'POST',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 200
{
"data": {
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "cancelled",
"note": null,
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [],
"images_count": 0,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 0,
"original_size": 0,
"optimized_size": 0,
"saved_bytes": 0,
"saved_percent": null,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"share_url": null,
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:20+00:00"
}
}
Download a page scan as a zip
/api/v1/scans/{id}/download
One zip with the optimized copy of every image of the scan that has finished, in the order the scanner found them. Images still compressing are left out, so wait for images_in_progress to reach 0 first.
The zip is laid out like the page: each image sits in the folders of its address (wp-content/uploads/hero.jpg), and an image from another host under a folder named after that host. It keeps its file name while its format is unchanged; a converted one takes its new extension. manifest.csv at the root lists every image address the scan fetched with its file in the zip, status, formats and sizes, then the addresses it passed over (a duplicate names the file it shares) and how many images your plan's per-scan limit left out.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl --fail-with-body -o scan.zip "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download" \
--connect-timeout 10 --max-time 1800 \
-H "Authorization: Bearer $IMINIFY_API_KEY"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.get(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
headers=HEADERS,
timeout=(10, 300),
)
response.raise_for_status()
with open("scan.zip", "wb") as out:
out.write(response.content)
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download', [
'sink' => 'scan.zip',
'timeout' => 1800,
]);
echo 'Saved scan.zip', PHP_EOL;
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download', {
signal: AbortSignal.timeout(1800_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
await writeFile('scan.zip', Buffer.from(await response.arrayBuffer()));
Response 200
The file itself, not JSON.
Share a page report
/api/v1/scans/{id}/share
Gives the scan's page report a public page at an address nobody can guess, returned as share_url. Anyone with the link can read the report as you see it, with thumbnails of the compressed images, without signing in; nothing on it can be downloaded or changed. The page is kept out of search engines. Comes with a paid plan: the link answers only while your account is on a paid plan, and comes back if it lapses and renews. Sharing a report that is already shared keeps its link.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl -X POST "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.post(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share",
headers=HEADERS,
timeout=(10, 30),
)
response.raise_for_status()
print(response.json())
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->post('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share');
print_r(json_decode((string) $response->getBody(), true));
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share', {
method: 'POST',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(await response.json());
Response 200
{
"data": {
"id": "9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7",
"url": "https://example.com/pricing",
"crawl_id": null,
"status": "completed",
"note": "Rendered in a headless browser. 14 image URLs found: 12 queued for compression, 1 duplicates of an image already queued, 1 in a format the compressor does not accept.",
"settings": {
"level": "smart",
"format": "webp",
"keep_metadata": false,
"width": 1600,
"height": null,
"scale": null
},
"skipped": [
{
"url": "https://cdn.example.com/pricing/hero.jpg?v=2",
"reason": "duplicate",
"message": "The same image as https://example.com/images/hero.jpg, so it was compressed once."
},
{
"url": "https://example.com/images/logo.svg",
"reason": "unsupported_format",
"message": "The file is SVG, a format Iminify does not compress."
}
],
"images_count": 12,
"images_in_progress": 0,
"alt_texts_in_progress": 0,
"images_finished": 12,
"original_size": 18406331,
"optimized_size": 2114902,
"saved_bytes": 16291429,
"saved_percent": 88.51,
"download_url": "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/download",
"share_url": "https://www.iminify.com/r/k3Xw9QmT2vLp8RzN4bYc6HdJ",
"created_at": "2026-10-01T11:05:00+00:00",
"updated_at": "2026-10-01T11:05:41+00:00"
}
}
Stop sharing a page report
/api/v1/scans/{id}/share
Switches the report's public link off. The page and its thumbnails answer 404 from the next request on. Sharing the report again later gives it a new link. Works on any plan, so a link can be taken down after a paid plan has ended.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl -X DELETE "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share" \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.delete(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share",
headers=HEADERS,
timeout=(10, 30),
)
response.raise_for_status()
print(response.status_code) # 204, nothing in the body
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->delete('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share');
echo $response->getStatusCode(), PHP_EOL; // 204, nothing in the body
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/share', {
method: 'DELETE',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
console.log(response.status); // 204, nothing in the body
Response 204
No content.
Download a page report as CSV
/api/v1/scans/{id}/report.csv
The page audit as a spreadsheet, one row per image address in the page's order: url, the format and bytes a browser received, natural_width and natural_height (the file's own size), rendered_width and rendered_height (the size it is shown at), above_fold, largest_paint (yes, no, or empty when not known), alt, findings (separated by spaces), wasted_bytes, outcome, skip_reason, then the original_bytes of the file the scan downloaded and the compressed_format and compressed_bytes of its compressed copy. The fields mean what they mean on the scan's audit. A cell that would start with =, +, - or @ starts with an apostrophe, so a spreadsheet never runs it. Comes with a paid plan, for a scan that has finished with a report.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl --fail-with-body -o report.csv "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.csv" \
--connect-timeout 10 --max-time 1800 \
-H "Authorization: Bearer $IMINIFY_API_KEY"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.get(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.csv",
headers=HEADERS,
timeout=(10, 300),
)
response.raise_for_status()
with open("report.csv", "wb") as out:
out.write(response.content)
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.csv', [
'sink' => 'report.csv',
'timeout' => 1800,
]);
echo 'Saved report.csv', PHP_EOL;
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.csv', {
signal: AbortSignal.timeout(1800_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
await writeFile('report.csv', Buffer.from(await response.arrayBuffer()));
Response 200
The file itself, not JSON.
Download a page report as PDF
/api/v1/scans/{id}/report.pdf
The page report printed for a client or a colleague: the page, how many images need fixing, each finding with how to fix it, the page's largest paint, the bytes that resizing would save and the formats received, then every image to fix with its findings and its compressed size. The CSV lists every image. Printing takes a second or two, and one PDF prints at a time on your account: a second request while one prints is answered with rate_limited. Comes with a paid plan, for a scan that has finished with a report.
Path parameters
-
idstring (id) required - The scan's id.
Request
curl --fail-with-body -o report.pdf "https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.pdf" \
--connect-timeout 10 --max-time 1800 \
-H "Authorization: Bearer $IMINIFY_API_KEY"
import os
import requests
API = "https://www.iminify.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
}
response = requests.get(
f"{API}/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.pdf",
headers=HEADERS,
timeout=(10, 300),
)
response.raise_for_status()
with open("report.pdf", "wb") as out:
out.write(response.content)
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'connect_timeout' => 10,
'timeout' => 30,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.pdf', [
'sink' => 'report.pdf',
'timeout' => 1800,
]);
echo 'Saved report.pdf', PHP_EOL;
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://www.iminify.com/api/v1/scans/9d3c4f12-7a8b-4c9d-a0e1-f2a3b4c5d6e7/report.pdf', {
signal: AbortSignal.timeout(1800_000),
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
await writeFile('report.pdf', Buffer.from(await response.arrayBuffer()));
Response 200
The file itself, not JSON.