The Iminify API lets your own code do everything the website does. Send an image and get back a smaller one, in the format and at the size you asked for. Give it the address of a web page and it finds every image the page loads and compresses them all. Rename, share or re-run what you made, and download one file or a zip of many.
It's JSON over HTTPS. Every request goes to https://www.iminify.com/api/v1, carries your API key, and gets JSON back, apart from downloads, which get the file.
In PHP, the PHP client makes each call for you and waits for the result, and in a Laravel app, Iminify for Laravel puts that client behind a facade and makes Spatie's Media Library conversions smaller.
Quickstart
You need an account with a verified email address. The free plan works; there is no separate API plan and nothing to pay for.
1. Create a key
Open Settings > API Tokens, give the key a name you'll recognise later (the script or the server it's for), and press Create. Copy the key straight away: it is shown once. Then put it in your environment, where every sample on these pages reads it from:
export IMINIFY_API_KEY="paste-your-key-here"
2. Check it works
curl "https://www.iminify.com/api/v1/account" \
-H "Authorization: Bearer $IMINIFY_API_KEY" \
-H "Accept: application/json"
A 200 with your plan in it means the key is good. A 401 means it was mistyped, and a 403 with email_not_verified means the account still has a verification email waiting.
3. Compress an image
This script uploads photo.jpg, asks for WebP at 1600 pixels wide, waits for the result and saves it. Pick your language; the choice sticks on every page.
#!/usr/bin/env bash
# Compress photo.jpg to WebP at 1600 pixels wide, wait for it, save the result.
# 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. Upload. The answer comes back straight away, queued, with the image's id.
id=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 1800 "$API/images" \
-H "$AUTH" -H "Accept: application/json" \
-F "[email protected]" -F "format=webp" -F "width=1600" \
| jq -r '.data.id')
# 2. Poll until it has finished.
while true; do
image=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 30 "$API/images/$id" \
-H "$AUTH" -H "Accept: application/json")
status=$(jq -r '.data.status' <<< "$image")
case "$status" in
completed|already-optimized) break ;;
failed|cancelled) echo "The image ended as $status" >&2; exit 1 ;;
esac
sleep 2
done
jq -r '.data | "\(.original.size) bytes -> \(.optimized.size) bytes (\(.saved_percent)% smaller)"' <<< "$image"
# 3. Download the optimized copy under the name it was given.
curl -sS --fail-with-body --connect-timeout 10 --max-time 1800 -o "$(jq -r '.data.name' <<< "$image")" \
"$API/images/$id/download" -H "$AUTH"
"""Compress photo.jpg to WebP at 1600 pixels wide, wait for it, save the result.
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 a file.
TIMEOUT = (10, 30)
FILE_TIMEOUT = (10, 300)
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
})
# 1. Upload. The answer comes back straight away, queued, with the image's id.
with open("photo.jpg", "rb") as file:
response = session.post(
f"{API}/images",
files={"file": file},
data={"format": "webp", "width": "1600"},
timeout=FILE_TIMEOUT,
)
response.raise_for_status()
image = response.json()["data"]
# 2. Poll until it has finished.
while image["status"] in ("queued", "optimizing"):
time.sleep(2)
response = session.get(f"{API}/images/{image['id']}", timeout=TIMEOUT)
response.raise_for_status()
image = response.json()["data"]
if image["status"] not in ("completed", "already-optimized"):
raise SystemExit(f"The image ended as {image['status']}")
print(f"{image['original']['size']} bytes -> {image['optimized']['size']} bytes ({image['saved_percent']}% smaller)")
# 3. Download the optimized copy under the name it was given.
response = session.get(image["optimized"]["download_url"], timeout=FILE_TIMEOUT)
response.raise_for_status()
with open(image["name"], "wb") as out:
out.write(response.content)
<?php
declare(strict_types=1);
// Compress photo.jpg to WebP at 1600 pixels wide, wait for it, save the result.
// 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 a file, 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',
],
]);
// 1. Upload. The answer comes back straight away, queued, with the image's id.
$response = $client->post('images', [
'timeout' => 1800,
'multipart' => [
['name' => 'file', 'contents' => fopen('photo.jpg', 'r'), 'filename' => 'photo.jpg'],
['name' => 'format', 'contents' => 'webp'],
['name' => 'width', 'contents' => '1600'],
],
]);
$image = json_decode((string) $response->getBody(), true)['data'];
// 2. Poll until it has finished.
while (in_array($image['status'], ['queued', 'optimizing'], true)) {
sleep(2);
$response = $client->get("images/{$image['id']}");
$image = json_decode((string) $response->getBody(), true)['data'];
}
if ( ! in_array($image['status'], ['completed', 'already-optimized'], true)) {
fwrite(STDERR, "The image ended as {$image['status']}\n");
exit(1);
}
echo "{$image['original']['size']} bytes -> {$image['optimized']['size']} bytes ({$image['saved_percent']}% smaller)\n";
// 3. Download the optimized copy under the name it was given.
$client->get("images/{$image['id']}/download", ['sink' => $image['name'], 'timeout' => 1800]);
// Compress photo.jpg to WebP at 1600 pixels wide, wait for it, save the result.
// Needs Node 20 or newer. Save it as compress.mjs and run: node compress.mjs
import { openAsBlob } from 'node:fs';
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 when it moves a file, 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',
},
});
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
return response;
};
// 1. Upload. The answer comes back straight away, queued, with the image's id.
const form = new FormData();
form.append('file', await openAsBlob('photo.jpg'), 'photo.jpg');
form.append('format', 'webp');
form.append('width', '1600');
let { data: image } = await (await call('/images', { method: 'POST', body: form }, 1800)).json();
// 2. Poll until it has finished.
while (['queued', 'optimizing'].includes(image.status)) {
await sleep(2000);
({ data: image } = await (await call(`/images/${image.id}`)).json());
}
if (!['completed', 'already-optimized'].includes(image.status)) {
throw new Error(`The image ended as ${image.status}`);
}
console.log(`${image.original.size} bytes -> ${image.optimized.size} bytes (${image.saved_percent}% smaller)`);
// 3. Download the optimized copy under the name it was given.
const download = await call(`/images/${image.id}/download`, {}, 1800);
await writeFile(image.name, Buffer.from(await download.arrayBuffer()));
That's the whole shape of the API: create something, poll it until it has finished, then fetch the result.
How it works
Work happens in the background. An upload answers straight away with 202 Accepted, the status queued and a Location header pointing at the image. A worker usually picks it up within seconds, the status moves to optimizing, and it ends as completed, already-optimized or failed, or as cancelled if someone calls it off while it waits (from the API or the website). Most images take a few seconds; a large photograph converted to AVIF can take a minute. Poll the image every second or two until the status changes. Page scans work the same way, over minutes rather than seconds.
It's your account, not a copy of it. Images you compress through the API show up in the results list on the website, and the other way round; an upload group's files are the API's alone. Both spend the same allowances: an image compressed on the website counts against the same daily and per-minute limits as one sent through the API. Your plan's limits are on the pricing page.
Nothing is deleted behind your back. An image and its optimized copy stay until you delete them, from the API or the website, unless you send it as ephemeral: then it goes about 24 hours after it finished, if you haven't deleted it yourself by then. An upload group whose image never arrived is deleted for you too.
What you can do
- Compress an image from a file or from its address, at one of four levels.
- Convert it to WebP, AVIF, PNG or JPG, or let
autotry each one and keep the smallest. - Resize it to a width, a height, both, or a percentage, and keep or strip its EXIF metadata.
- Download the optimized copy or the original, or several at once as a zip.
- Compress it again with other settings, retry a failure, or cancel one that hasn't started.
- Rename one image, or a batch with numbers in upload order.
- Share a result on a public page, and switch the link off again.
- Send one image and get every size and format your site keeps of it made from it, counted as one image.
- Scan a page for every image it loads, and download them all as one zip.
- Crawl a site on a paid plan, several pages from its sitemap or a list, with one report and one zip for the whole site.
- Check a crawled site every week on a paid plan, compressing only its new or changed images, with an email when a check finds some.
- Read your plan, limits and usage.
Conventions
- Every request carries
Authorization: Bearer <key>; Authentication has the details. - Send
Accept: application/json. Errors come back as JSON either way, in one shape. - Bodies are JSON, except an upload (an image, or an upload group's source), which is
multipart/form-data. - Ids are UUIDs. Sizes are in bytes, dimensions in pixels, and times in ISO 8601, in UTC.
- Lists come a page at a time, 15 to a page unless you ask for up to 100 with
per_page, withlinksandmetato find the rest.
For tools and agents
The whole API is described in an OpenAPI 3.1 document at https://www.iminify.com/api/v1/openapi.json, with every sample on these pages inside it. Import it into Postman, Insomnia or a client generator, or hand it to an AI agent that should call the API for you. It needs no key.
For an AI app like Claude, ChatGPT or Cursor there's a shorter way: connect it to the MCP server and ask for what you want in plain words.