Reference

Optimize API

POST /v1/optimize — quality levels, target formats, resize options, and what the response looks like.

Request

POST /v1/optimize
POST /v1/optimize?async=true

multipart/form-data with two fields:

FieldRequiredDescription
imageyesThe image file.
datanoA JSON string with the options below.
Terminal
curl -X POST https://api.helloimg.io/v1/optimize \
  -H "Authorization: Bearer <your API key>" \
  -F "[email protected]" \
  -F 'data={"level":"ultra","convert":"avif","resize":{"width":1600}}'

data fields

FieldTypeDefaultDescription
levelstringnormallossless | normal | aggressive | ultra — an alias for the boolean flags below.
normal / aggressive / ultrabooleannormal: trueThe same levels as booleans, if you'd rather not use level.
losslessbooleanfalseEncode without lossy compression. For a JPEG target this produces a PNG instead — JPEG has no lossless mode.
convertstring | nullnullTarget format: jpeg, jpg, png, webp, avif, or gif. Omit to keep the source format.
keep_exifbooleantrueKeep EXIF metadata (camera, GPS, etc.) in the output.
resizeobjectSee below.

level and the boolean flags both work — level just sets the corresponding flag for you. If both are sent, the explicit boolean wins.

resize

FieldTypeDescription
widthnumberTarget width in pixels.
heightnumberTarget height in pixels.
percentnumberResize by percentage instead of absolute dimensions.
smartcropbooleanSubject-aware crop to an exact width x height instead of a proportional resize — see Smart crop. Requires both width and height.

As a shortcut, you can send max_width / max_height at the top level of data instead of a resize object — it fits the image inside that box without upscaling, and is equivalent to resize: { width, height }.

Response

No async flag (default): the request blocks until the job finishes and returns the result directly — 200 on success, 422 if processing failed, 408 if it didn't finish within the timeout (poll /v1/jobs/{job_id} yourself instead, using the job_id and poll_url in the timeout body).

?async=true: returns immediately with 202 and a job to poll:

{
  "job_id": "job_5f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c",
  "status": "processing",
  "poll_url": "/v1/jobs/job_5f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}

Either way, if the exact same image and options were already optimized recently, you get the cached result back immediately with 200 instead of a job — see Job polling for the result shape.

Multiple formats in one call

POST /v1/optimize/multi takes the same image field, but data carries a formats array (up to 5) instead of a single convert/level:

Terminal
curl -X POST "https://api.helloimg.io/v1/optimize/multi?async=true" \
  -H "Authorization: Bearer <your API key>" \
  -F "[email protected]" \
  -F 'data={"formats":[{"format":"jpeg","level":"normal"},{"format":"webp","level":"aggressive"},{"format":"avif","level":"aggressive"}]}'

Each entry in formats is { "format": "jpeg" | "png" | "webp" | "avif", "level"?: string }. This produces one job per format in parallel; poll the returned job_id the same way as a single optimize call — see Job polling for the multi-format response shape.

resize (and its max_width / max_height alias, including smartcrop) work the same way here as on a single-format call — set it at the top level of data, alongside formats, and it applies to every format in the batch.

Unlike the top-level level field, an unrecognized level string inside one of the formats[] entries is not rejected — it's silently treated as normal. Double-check spelling ("ultra", not "utlra") rather than relying on a 400 to catch it.

Multi-format errors

/v1/optimize/multi validates its formats array before the shared sizing/format checks below run:

StatusBodyCause
400{ "error": "Missing 'data' field with formats array" }No data field, or it has no formats array.
400{ "error": "At least one format required" }formats is an empty array.
400{ "error": "Maximum 5 formats allowed" }formats has more than 5 entries.
400{ "error": "Invalid format: ..." }An entry's format isn't jpeg, jpg, png, webp, or avif. Unlike single-format /v1/optimize, gif isn't accepted here.

Errors

StatusBodyCause
400{ "error": "Missing 'image' field" }No image file in the request.
400{ "error": "Unsupported convert target: ...", "code": "invalid_request", "supported": [...] }convert isn't one of the supported targets.
400{ "error": "...", "code": "invalid_resize" }resize.smartcrop was sent without both width and height, or combined with percent.
400{ "error": "Invalid level: ...", "code": "invalid_level" }level isn't one of the four accepted values.
413{ "error": "File size exceeds 10MB limit", "code": "file_too_large", "max_size": 10485760 }Image is larger than 10MB.
401{ "error": "invalid api key" }Missing or malformed Authorization header. Applies to every /v1/* route — one shared check in front of all of them.
429{ "error": "monthly image limit reached", "code": "limit_reached" }Free plan, 1,000 images already processed this calendar month — see Plans & limits.