Reference

Upload groups API

Send one image and get every size and format your site keeps made from it, counted as one image. Open a group, send its source, poll it and download the files.

An upload group is one image and every size and format you keep of it, the way a WordPress site keeps a thumbnail, a medium and a large copy of each upload, often in WebP too. You describe the image and the outputs you want, send the image once, and Iminify makes every output from it. The whole group counts as one image.

Groups are for a site or an app that stores several sizes of each upload. To compress one file as it is, send it as an image instead.

How a group counts

A group spends one of your daily images and one of your compressions a minute, whatever it makes. Each output in each format is one file, and files have a daily limit of their own: a group with five outputs in its own format and WebP makes ten files. The files a day for every plan are on the pricing page, and yours are in GET /account under limits.files_per_day and usage.files.

Every limit is checked when the group opens, all at once: the source against your plan's file size and 64 megapixels, the outputs against the rules below, the image, the minute and the files. A group past any of them is refused whole and nothing is counted. A group that opens is never refused part-way, and it spends its image and its files there and then. An AI alt text, when you ask for one, is written for the main output only and counts one against your daily AI alt texts.

Your groups can also only have so many files waiting to be made at once. Past that, a new group is refused with rate_limited and a Retry-After header; wait that long and open it again.

The life of a group

TEXT
awaiting_source ──► in_progress ──► finished
       │
       └──► expired

A group opens awaiting_source, and takes its image until upload_by, 60 minutes after it opened. Once the image arrives it turns in_progress, and finished once every file has ended. A group whose image never came reads expired: it makes nothing, and shortly afterwards it is deleted for you and the image and files it counted are given back.

Opening a group

POST /groups describes the image you are about to send in source (its format, bytes, width and height) and lists the outputs to make from it:

JSON
{
    "name": "IMG_2041",
    "source": { "format": "jpg", "size": 2841205, "width": 2560, "height": 1707 },
    "outputs": [
        { "name": "full", "width": 2560, "height": 1707, "size": 2841205 },
        { "name": "medium", "width": 300, "height": 200, "size": 21204 },
        { "name": "thumbnail", "width": 150, "height": 150, "crop": ["center", "center"], "size": 7311 }
    ],
    "formats": ["original", "webp"]
}
  • source has to match the file you send exactly, its width and height as displayed (with its orientation applied). A group takes a JPG, PNG, WebP or GIF.
  • outputs name each size, the main one first. An output without crop keeps the source's shape and is scaled to its width and height. One with crop is scaled to cover its size and then cut at the anchors you give, across then down: start, center or end. That's how WordPress crops a size, with crop: true as ["center", "center"].
  • size on an output is the bytes of the copy you already have, if any. A made file replaces your copy only when it is smaller.
  • formats are the formats to make each output in: original is the source's own, webp and avif a copy beside it. Leave it out for original alone.

The answer is the group with an upload_by. Next, send the image.

The rules

Iminify makes every output itself, so a group can only ever be one picture's work. A group that breaks any of these is refused with outputs_refused, and the message names the rule:

  • each output at most the source's width and height, since nothing is enlarged;
  • an output without crop keeps the source's shape, within the pixel its rounding takes;
  • at most 30 outputs, at most 12 of them cropped, and a cropped one no more than 2,560 pixels on its longer side;
  • the outputs' pixels together at most 6 times the source's;
  • every output named once, whatever the case: the names become file names.

Sending the image

POST /groups/{id}/source takes the file as a multipart file, once, before upload_by. It goes through the checks every upload does, and it has to be the image the group was opened for. A different file is refused and the group still takes the right one until upload_by. A second upload, or one after upload_by, is answered with group_closed.

The answer comes straight back in_progress. Poll GET /groups/{id} a few seconds apart until it is finished. Background work can take longer: see Bulk work for priority.

What comes back

outputs lists each output in each format with its own status, its made file's width, height and bytes, and replace. Write a file over your copy only where replace is true: it finished at the output's size and is smaller than the copy you have, or you have none. A WebP or AVIF is marked only when it is smaller than the file you keep for that size, so you never serve a format copy heavier than the image it stands in for.

A failed or cancelled file is never marked. One that ends already-optimized came out no smaller than the source, which is handed back as it is, so it is marked only for an output the source's own size and lighter than your copy.

The zip

GET /groups/{id}/download answers once the group has finished. It holds every file marked replace, named after its output and format (thumbnail.webp), and a manifest.csv with a row for every output in every format: output, format, file (empty when it isn't in the zip), status, width, height, bytes and replace. A group with nothing to replace answers with the manifest alone. Each file's file in outputs is its name in the zip.

Deleting a group

DELETE /groups/{id} deletes the group, every file it made and its source, for good. It gives back nothing the group counted; only an image that never arrived does that.

Send ephemeral when you open the group and it is deleted for you about 24 hours after its last file finished, if you haven't deleted it by then. Delete it yourself once you've saved its files: a download alone deletes nothing. See Deleting images once saved.

A group's files are its own: they're not in the image list, the website's results or a zip of several images, and the image calls answer their ids with not_found. No webhook is sent for a group; poll it.

The Group object

id string
The group's id.
name string
The image's name, as you sent it.
status string
upload_by timestamp or null
The last moment the group takes its source, 60 minutes after it opened, or null once it has it.
source object
The source the group was opened for.
source.format string
Its format.
source.size integer
Its size in bytes.
source.width integer
Its width in pixels.
source.height integer
Its height in pixels.
settings object
The settings every output is made with, as they were sent.
settings.level string
The compression level.
settings.formats array
The formats each output is made in, the source's own first.
settings.keep_metadata boolean
Whether the EXIF metadata is kept.
settings.ephemeral boolean
Whether the group is deleted for you about 24 hours after its last file finished, if you haven't deleted it by then.
settings.priority string
files integer
Files the group makes: every output in every format. Each counts one against your daily files.
outputs array
Each output in each of its formats, the main one first, or an empty list until the source arrives.
alt_text object or null
The AI alt text and suggested file name for the main output, or null when none was asked for, or until the source arrives.
alt_text.status string
pending: being written; it arrives a few seconds after the main output finishes. written: ready. failed: the model gave no usable answer, or something broke. skipped: your daily AI alt texts were used up when the source arrived.
alt_text.text string or null
One sentence for the image's alt attribute, or null until it is written.
alt_text.language string or null
The language tag it was written in.
alt_text.suggested_name string or null
A short descriptive file name without an extension, or null until it is written or when the model offered none.
download_url string or null
Where to download the group's files as one zip, once it has finished; null until then.
created_at timestamp
When the group was opened.
received_at timestamp or null
When its source arrived, or null.

The GroupOutput object

name string
The output's name.
format string
The format asked for: original, webp or avif.
status string
Where this file is: queued, optimizing, completed, already-optimized (nothing came out smaller than the source, which is handed back), failed or cancelled.
file string or null
Its name in the group's zip, like thumbnail.webp, or null when it isn't there: only a file marked replace is.
width integer or null
The made file's width in pixels, or null until it has finished.
height integer or null
The made file's height in pixels, or null until it has finished.
size integer or null
The made file's size in bytes, or null until it has finished.
replace boolean
Whether to write this file over your copy: it finished at the output's width and height and is smaller than the copy you have (the size you sent), or you have none. A WebP or AVIF is marked only when it is smaller than the file you keep for that output.

Open an upload group

POST /api/v1/groups

Declares one image and the outputs to make from it: each a name, a width and a height, a crop for a cropped size, and the bytes of the copy you already have, in each of the formats. Nothing is uploaded yet. The group answers with the status awaiting_source and an upload_by 60 minutes away: send the image to POST /groups/{id}/source before then.

Every limit is checked here, at once: the source against your plan's file size and 64 megapixels, the outputs against the group's rules, one of your daily images and of your compressions a minute, a file for each output in each format against your daily files, and the files your groups already have waiting. A group past any of them is refused whole with nothing counted; one taken spends its image and its files now, and is never refused part-way. A source that never comes in time gives them back. See Upload groups.

Body (JSON)

name
string required
The image's name, without its extension, like IMG_2041.
source
object required
The image you will send as the group's source, described exactly: the upload is refused unless its format, its bytes and its width and height (as displayed, with its orientation applied) are these.
source.format
string required
Its format: jpg, png, webp or gif. jpeg is read as jpg.

jpg jpeg png webp gif

source.size
integer required
Its size in bytes, at most your plan's file size.
source.width
integer required
Its width in pixels. 1 to 65535
source.height
integer required
Its height in pixels. 1 to 65535
outputs
array of objects required
The outputs to make, the main one first: up to 30, each at most the source's width and height. The first output in the source's own format is the one an alt text is written for.
outputs[].name
string required
The output's name, like thumbnail or medium_large: letters, digits, dots, hyphens and underscores, up to 64, unique in the group whatever their case. It names the output's files, thumbnail.webp in the zip.
outputs[].width
integer required
The output's width in pixels. 1 to 65535
outputs[].height
integer required
The output's height in pixels. 1 to 65535
outputs[].crop
array of strings
Where to cut a cropped output, across then down: start, center or end each, like ["center", "center"]. The source is scaled to cover the output, then cut at these anchors, as WordPress crops a size. Leave it out for an output that keeps the source's shape.
outputs[].size
integer
The bytes of the copy of this output you already have, if any. A file made for the output replaces your copy only when it is smaller.
formats
array of strings
The formats to make each output in: original is the source's own, webp and avif a copy beside it. A WebP source's webp is its own format, made once. Leave it out for original alone.
level
string
How hard to compress every output. smart finds the lowest quality that still looks like the source, ultra goes further and softens fine texture a little, lossless changes no pixel, none only resizes, crops and converts. See Compression settings.

none lossless ultra smart

Default: smart
keep_metadata
boolean
Keep the EXIF metadata (camera, date, location) in the outputs. Off by default, which strips it; the orientation is applied to the pixels first. Default: false
alt_text
boolean
Write an AI alt text and a suggested file name for the main output once the source arrives. Off by default for a group. A small copy of the image goes to Anthropic, whose Claude model writes both; it counts one against your daily AI alt texts, and once those are used up the group still runs, without one. See Alt text and file names. Default: false
alt_text_language
string
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.

en es fr de it pt nl pl sv tr ru uk ar fa he hi id vi th ja ko zh-Hans zh-Hant

Default: en
ephemeral
boolean
Delete the group once you've saved its files, for a caller that keeps its own copies. Send DELETE /groups/{id} once you've written them; a group you leave is deleted for you about 24 hours after its last file finished, its files and its source with it. Off by default, which keeps the group until you delete it. See Deleting images once saved. Default: false
priority
string
background for bulk work nobody waits on, such as a whole library: its files are made after every image sent without it, on every plan, and count against your limits the same. See Bulk work.

interactive background

Default: interactive

Request

curl -X POST "https://www.iminify.com/api/v1/groups" \
  --connect-timeout 10 --max-time 30 \
  -H "Authorization: Bearer $IMINIFY_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"IMG_2041","source":{"format":"jpg","size":2841205,"width":2560,"height":1707},"outputs":[{"name":"full","width":2560,"height":1707,"size":2841205},{"name":"medium","width":300,"height":200,"size":21204},{"name":"thumbnail","width":150,"height":150,"crop":["center","center"],"size":7311}],"formats":["original","webp"],"ephemeral":true}'

Response 201

JSON
{
    "data": {
        "id": "9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24",
        "name": "IMG_2041",
        "status": "awaiting_source",
        "upload_by": "2026-10-01T10:40:00+00:00",
        "source": {
            "format": "jpg",
            "size": 2841205,
            "width": 2560,
            "height": 1707
        },
        "settings": {
            "level": "smart",
            "formats": [
                "original",
                "webp"
            ],
            "keep_metadata": false,
            "ephemeral": true,
            "priority": "interactive"
        },
        "files": 6,
        "outputs": [],
        "alt_text": null,
        "download_url": null,
        "created_at": "2026-10-01T09:40:00+00:00",
        "received_at": null
    }
}

Send a group's source

POST /api/v1/groups/{id}/source

The image the group was opened for, as a multipart file, once, before upload_by. It has to be that image exactly: its format, its bytes, and its width and height as displayed. It also goes through the checks every upload does. A file that differs is refused, and the group still takes the right one until upload_by. The answer comes straight back with the status in_progress and a Location header; every output is made in the background, so poll the group until its status is finished.

An animated WebP or PNG can't be resized: a group that makes any output at another size from one is refused with animation_not_resizable. An animated GIF's cropped outputs are its first frame.

Path parameters

id
string (id) required
The group's id.

Body (multipart/form-data, or JSON for a url)

file
file required
The source image: the format, bytes and dimensions the group was opened with.

Request

curl -X POST "https://www.iminify.com/api/v1/groups/9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24/source" \
  --connect-timeout 10 --max-time 1800 \
  -H "Authorization: Bearer $IMINIFY_API_KEY" \
  -H "Accept: application/json" \
  -F "file=@IMG_2041.jpg"

Response 202

JSON
{
    "data": {
        "id": "9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24",
        "name": "IMG_2041",
        "status": "in_progress",
        "upload_by": null,
        "source": {
            "format": "jpg",
            "size": 2841205,
            "width": 2560,
            "height": 1707
        },
        "settings": {
            "level": "smart",
            "formats": [
                "original",
                "webp"
            ],
            "keep_metadata": false,
            "ephemeral": true,
            "priority": "interactive"
        },
        "files": 6,
        "outputs": [
            {
                "name": "full",
                "format": "original",
                "status": "completed",
                "file": "full.jpg",
                "width": 2560,
                "height": 1707,
                "size": 612480,
                "replace": true
            },
            {
                "name": "full",
                "format": "webp",
                "status": "completed",
                "file": "full.webp",
                "width": 2560,
                "height": 1707,
                "size": 401226,
                "replace": true
            },
            {
                "name": "medium",
                "format": "original",
                "status": "queued",
                "file": null,
                "width": null,
                "height": null,
                "size": null,
                "replace": false
            },
            {
                "name": "medium",
                "format": "webp",
                "status": "queued",
                "file": null,
                "width": null,
                "height": null,
                "size": null,
                "replace": false
            },
            {
                "name": "thumbnail",
                "format": "original",
                "status": "queued",
                "file": null,
                "width": null,
                "height": null,
                "size": null,
                "replace": false
            },
            {
                "name": "thumbnail",
                "format": "webp",
                "status": "queued",
                "file": null,
                "width": null,
                "height": null,
                "size": null,
                "replace": false
            }
        ],
        "alt_text": {
            "status": "pending",
            "text": null,
            "language": "en",
            "suggested_name": null
        },
        "download_url": null,
        "created_at": "2026-10-01T09:40:00+00:00",
        "received_at": "2026-10-01T09:40:21+00:00"
    }
}

Get an upload group

GET /api/v1/groups/{id}

One group with each output in each of its formats: its status, the made file's size and dimensions, and replace, whether to write it over your copy. Poll this while a group is in_progress; once a few seconds apart is plenty. A group whose source never came reads expired until it is deleted for you, shortly after.

Path parameters

id
string (id) required
The group's id.

Request

curl "https://www.iminify.com/api/v1/groups/9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24" \
  --connect-timeout 10 --max-time 30 \
  -H "Authorization: Bearer $IMINIFY_API_KEY" \
  -H "Accept: application/json"

Response 200

JSON
{
    "data": {
        "id": "9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24",
        "name": "IMG_2041",
        "status": "finished",
        "upload_by": null,
        "source": {
            "format": "jpg",
            "size": 2841205,
            "width": 2560,
            "height": 1707
        },
        "settings": {
            "level": "smart",
            "formats": [
                "original",
                "webp"
            ],
            "keep_metadata": false,
            "ephemeral": true,
            "priority": "interactive"
        },
        "files": 6,
        "outputs": [
            {
                "name": "full",
                "format": "original",
                "status": "completed",
                "file": "full.jpg",
                "width": 2560,
                "height": 1707,
                "size": 612480,
                "replace": true
            },
            {
                "name": "full",
                "format": "webp",
                "status": "completed",
                "file": "full.webp",
                "width": 2560,
                "height": 1707,
                "size": 401226,
                "replace": true
            },
            {
                "name": "medium",
                "format": "original",
                "status": "completed",
                "file": "medium.jpg",
                "width": 300,
                "height": 200,
                "size": 14880,
                "replace": true
            },
            {
                "name": "medium",
                "format": "webp",
                "status": "completed",
                "file": "medium.webp",
                "width": 300,
                "height": 200,
                "size": 11402,
                "replace": true
            },
            {
                "name": "thumbnail",
                "format": "original",
                "status": "completed",
                "file": "thumbnail.jpg",
                "width": 150,
                "height": 150,
                "size": 6950,
                "replace": true
            },
            {
                "name": "thumbnail",
                "format": "webp",
                "status": "completed",
                "file": null,
                "width": 150,
                "height": 150,
                "size": 7402,
                "replace": false
            }
        ],
        "alt_text": {
            "status": "written",
            "text": "A red tram crossing a stone bridge at dusk",
            "language": "en",
            "suggested_name": "red-tram-stone-bridge-dusk"
        },
        "download_url": "https://www.iminify.com/api/v1/groups/9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24/download",
        "created_at": "2026-10-01T09:40:00+00:00",
        "received_at": "2026-10-01T09:40:21+00:00"
    }
}

Delete an upload group

DELETE /api/v1/groups/{id}

Deletes the group, every file it made and its source, for good. A group with a file still queued or being made is refused; wait for it to finish. Deleting a group gives back nothing it counted: only a source that never came does that.

Path parameters

id
string (id) required
The group's id.

Request

curl -X DELETE "https://www.iminify.com/api/v1/groups/9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24" \
  --connect-timeout 10 --max-time 30 \
  -H "Authorization: Bearer $IMINIFY_API_KEY" \
  -H "Accept: application/json"

Response 204

No content.

Download an upload group as a zip

GET /api/v1/groups/{id}/download

Every file marked replace, each named after its output and format (thumbnail.webp), and a manifest.csv listing every output in every format with its status, width, height, bytes and replace. A file that doesn't replace your copy isn't in the zip, and a group with nothing to replace answers with the manifest alone. It is ready once the group has finished, so a group still at work is refused.

Path parameters

id
string (id) required
The group's id.

Request

curl --fail-with-body -o group.zip "https://www.iminify.com/api/v1/groups/9d3c5c02-4e1f-4a6b-8d2c-3f5a7b9c1e24/download" \
  --connect-timeout 10 --max-time 1800 \
  -H "Authorization: Bearer $IMINIFY_API_KEY"

Response 200

The file itself, not JSON.