API rate limits and quotas
Requests a minute, compressions a minute, images and page scans a day, file size and batch size for each Iminify plan, and how to handle a 429.
Two kinds of limit apply: how fast you call (per minute) and how much you compress (per day). Both belong to your account and your plan, not to a key, so two keys on one account share them.
What your plan sets
Your plan decides:
- how many images you can compress a day, and how many page scans you can run,
- how many images one page scan compresses,
- the largest file you can upload or have fetched,
- how many compressions and how many page scans you can start a minute,
- how many API requests you can make a minute.
The numbers for every plan are on the pricing page. Your own, with what you've used today, are one call away at GET /account, under limits and usage, so a script can read them rather than hard-code them.
Three limits are the same on every plan: an image can have at most 64 megapixels, before and after a resize, a batch call takes at most 150 ids, and Iminify fetches at most 2 image addresses for you at a time.
The API is for accounts with a verified email address; a guest, or an account that hasn't verified yet, can't use it.
Per minute
Requests a minute counts every call you make to the API, reads included. Compressions a minute counts every compression queued, which is an upload, a recompress or a retry, and page scans a minute counts every scan started or retried. All three windows are a minute long and counted per account.
The compressions and the page scans are the same counts the website spends. An image you drop on the website and one you send to the API come out of one minute, not one each, so running both at once never gets you more than your plan allows.
Every answer carries your request limit in its X-RateLimit-Limit header and what is left of the minute in X-RateLimit-Remaining, so you can slow down before you hit it.
Go over any of the three and the answer is 429 with the code rate_limited and a Retry-After header in seconds. Wait that long and send the same request again. Nothing was done and nothing was counted.
The same answer comes back when you send an image address while 2 of yours are still being fetched. That isn't a per-minute count: it clears as soon as one of them finishes, usually within seconds.
Per day
The daily counts are a rolling 24 hours that start with your first image (or scan), not midnight. usage.images.resets_at in GET /account says when yours opens again.
- An image counts once it's queued: an upload, a recompress or a retry. A refused request, a failed validation or a storage hiccup costs nothing.
- A page scan counts one scan. The images it finds don't count against your daily images; they're held to the plan's images per scan instead.
- The website and the API spend the same counts.
When the day's count is spent, the answer is 429 with the code daily_limit_reached and a Retry-After header holding the seconds until the window opens again. That can be hours, so don't retry it in a loop. A plan without a daily cap is still a service shared with other people, so traffic that degrades it for everyone can be slowed down.
Handling a 429
Read Retry-After, wait, send the request again. For rate_limited that's seconds; for daily_limit_reached it can be most of a day, so decide whether your script should wait or stop.
#!/usr/bin/env bash
# Send a request, and when the API answers 429, wait as long as it asks and send it again.
set -euo pipefail
API="https://www.iminify.com/api/v1"
for attempt in 1 2 3 4 5; do
status=$(curl -sS -o response.json -D headers.txt -w '%{http_code}' "$API/account" \
-H "Authorization: Bearer $IMINIFY_API_KEY" -H "Accept: application/json")
if [ "$status" != "429" ]; then
cat response.json
exit 0
fi
# A daily limit can ask for hours; decide whether your script should wait that long.
wait=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
sleep "${wait:-60}"
done
exit 1
"""Send a request, and when the API answers 429, wait as long as it asks and send it again.
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"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['IMINIFY_API_KEY']}",
"Accept": "application/json",
})
def call(method, path, attempts=5, **kwargs):
for _ in range(attempts):
response = session.request(method, f"{API}{path}", **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response
# A daily limit can ask for hours; decide whether your script should wait that long.
time.sleep(int(response.headers.get("Retry-After", 60)))
response.raise_for_status()
print(call("GET", "/account").json()["data"]["usage"])
<?php
declare(strict_types=1);
// Send a request, and when the API answers 429, wait as long as it asks and send it again.
// Needs PHP 8.1 or newer and Guzzle (composer require guzzlehttp/guzzle).
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
$client = new Client([
'base_uri' => 'https://www.iminify.com/api/v1/',
'http_errors' => false,
'headers' => [
'Authorization' => 'Bearer ' . getenv('IMINIFY_API_KEY'),
'Accept' => 'application/json',
],
]);
function call(Client $client, string $method, string $path, array $options = [], int $attempts = 5): ResponseInterface
{
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
$response = $client->request($method, $path, $options);
if (429 !== $response->getStatusCode()) {
if ($response->getStatusCode() >= 400) {
throw new RuntimeException($response->getStatusCode() . ': ' . json_decode((string) $response->getBody(), true)['message']);
}
return $response;
}
// A daily limit can ask for hours; decide whether your script should wait that long.
sleep((int) ($response->getHeaderLine('Retry-After') ?: 60));
}
throw new RuntimeException('Still rate limited after every attempt');
}
$response = call($client, 'GET', 'account');
print_r(json_decode((string) $response->getBody(), true)['data']['usage']);
// Send a request, and when the API answers 429, wait as long as it asks and send it again.
// Needs Node 20 or newer. Save it as retry.mjs and run: node retry.mjs
import { setTimeout as sleep } from 'node:timers/promises';
const API = 'https://www.iminify.com/api/v1';
const call = async (path, options = {}, attempts = 5) => {
for (let attempt = 1; attempt <= attempts; attempt++) {
const response = await fetch(`${API}${path}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.IMINIFY_API_KEY}`,
Accept: 'application/json',
},
});
if (response.status !== 429) {
if (!response.ok) {
throw new Error(`${response.status}: ${(await response.json()).message}`);
}
return response;
}
// A daily limit can ask for hours; decide whether your script should wait that long.
await sleep(Number(response.headers.get('Retry-After') ?? 60) * 1000);
}
throw new Error('Still rate limited after every attempt');
};
console.log((await (await call('/account')).json()).data.usage);
Polling without wasting requests
Polling one image every two seconds costs 30 requests a minute, which leaves most plans plenty of room; your own limit is in X-RateLimit-Limit. For a batch, poll the list with status=optimizing or status=queued instead of each image, or poll only the images you still wait for. A page scan needs one poll every five seconds or so: GET /scans/{id} carries every image's status at once.