Image & SVG
Image Compressor API
GET request to /v1/image-compressor, returning json in the standard envelope.
/v1/image-compressor3 credits / call + 1 per megapixel Free tierAuthentication
Send your API key as a bearer token. Create one free in the dashboard.
Authorization: Bearer $TOOLSXPO_KEYParameters
Send these as a JSON body, as query-string parameters, or as form fields alongside uploaded files (-F file=@photo.jpg, up to 10). A raw image body works too, with the options in the query string. Every request needs one image source: image, imageUrl, an uploaded file, or images for a batch. Fields marked required must be present; everything else has a sensible default.
| Name | Type | Default | Description |
|---|---|---|---|
| image | string | — | The image as base64, or a data: URL (data:image/png;base64,...). JPEG, PNG, WebP, AVIF, GIF or HEIC. |
| imageUrl | string | — | Public http(s) URL to fetch the image from, instead of sending it inline. Max 20 MB. |
| name | string | — | Optional label echoed back in the result, e.g. the file name. |
| images | array of object1–10 | — | Batch mode: up to 10 images, each { image | imageUrl, name? }. Also accepts a list of URLs, or a Source Files step's output. The same options apply to all of them. |
| images[].image | string | — | The image as base64, or a data: URL (data:image/png;base64,...). JPEG, PNG, WebP, AVIF, GIF or HEIC. |
| images[].imageUrl | string | — | Public http(s) URL to fetch the image from, instead of sending it inline. Max 20 MB. |
| images[].name | string | — | Optional label echoed back in the result, e.g. the file name. |
| quality | integer1–100 | — | 1–100, lower is smaller. Defaults: JPEG 75, WebP 75, AVIF 55. PNG is compressed losslessly. |
| targetKb | integer1–20480 | — | Aim for at most this many KB. Searches quality downward from `quality`; reports the quality it settled on. |
| format | string | auto | auto keeps each image's own format (GIF becomes PNG, HEIC becomes JPEG). Or force jpeg, png, webp or avif.One of: auto, jpeg, png, webp, avif |
| maxWidth | integer1–12000 | — | Shrink to at most this many pixels wide, keeping the aspect ratio. Never enlarges. |
| maxHeight | integer1–12000 | — | Shrink to at most this many pixels high, keeping the aspect ratio. Never enlarges. |
| background | string | #ffffff | Colour placed behind transparent areas when the output format has no transparency (JPEG). |
| output | string | url | url (default): each image comes back as a download link, kept for your plan's file retention. base64: the image inline in the JSON. Add ?output=binary to the request URL to get the file itself instead of JSON.One of: url, base64 |
Example request
curl "https://api.toolsxpo.com/v1/image-compressor?image=iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5%2BAAAAL0lEQVR42mNgAIIojRP%2FpwHxJSBmAAtEAQWA%2BFIUTKAHKADEl3pgAvuAAkB8CYgB9cMhwU8NlV0AAAAASUVORK5CYII%3D&quality=70&format=auto&background=%23ffffff&output=url" \
-H "Authorization: Bearer $TOOLSXPO_KEY"Response
Every endpoint answers with the same envelope: the result in data, what the call cost and your standing in meta. One entry per image in results, totals in summary. By default each image is a download link.
{
"ok": true,
"data": {
"results": [
{
"index": 0, "name": "photo.jpg", "ok": true,
"inputFormat": "jpeg", "format": "webp", "contentType": "image/webp",
"width": 1600, "height": 1067, "inputWidth": 4000, "inputHeight": 2667,
"inputBytes": 3104822, "bytes": 143210, "savedPercent": 95.4,
"quality": 80, "lossless": false, "unchanged": false,
"engine": "images", "warnings": [], "billableMegapixels": 3.84,
"url": "https://ai-cdn.toolsxpo.com/…/image-outputs/9f2c….webp",
"expiresAt": "2026-10-22T14:54:19.772Z"
},
{ "index": 1, "name": "broken.png", "ok": false, "error": "Not a supported image. Send JPEG, PNG, WebP, AVIF, GIF or HEIC." }
],
"summary": {
"total": 2, "succeeded": 1, "failed": 1,
"inputBytes": 3104822, "outputBytes": 143210, "savedPercent": 95.4,
"billableMegapixels": 3.84, "engines": { "images": 1 }
},
"_meter": { "unit": "megapixel", "quantity": 3.84 }
},
"meta": {
"tool": "image-compressor", "credits": 5, "baseCredits": 5, "billedFrom": "plan",
"pricing": { "mode": "metered", "meterUnit": "megapixel", "creditsPerUnit": 1 },
"quota": { "limit": 30000, "used": 412, "remaining": 29588, "resetsAt": "2026-11-01T00:00:00.000Z", "scope": "account", "plan": "pro" }
}
}data fields
| Field | Type | Description |
|---|---|---|
| results | array | One entry per input image, in the order sent. |
| results[].index | number | Position of the image in the request (0-based). |
| results[].name | string | null | The `name` you sent, or the uploaded file name. |
| results[].ok | boolean | `false` if this image failed; the others still succeed. A failed image costs nothing. |
| results[].error | string | Why this image failed. Present only when `ok` is false. |
| results[].url | string | Download link for the converted image (default `output: "url"`). Public but unguessable. |
| results[].expiresAt | string (ISO 8601) | When `url` stops working — your plan's file retention (1–20 days). |
| results[].base64 | string | The converted image inline, when you sent `output: "base64"` (or if storing failed — see `warnings`). |
| results[].format | "jpeg" | "png" | "webp" | "avif" | Format of the output. |
| results[].contentType | string | MIME type of the output, e.g. `image/webp`. |
| results[].width | number | Output width in pixels. |
| results[].height | number | Output height in pixels. |
| results[].inputFormat | string | Detected format of the input (jpeg, png, webp, avif, gif, heic…). |
| results[].inputWidth | number | Input width in pixels (after EXIF rotation). |
| results[].inputHeight | number | Input height in pixels. |
| results[].inputBytes | number | Size of the input file. |
| results[].bytes | number | Size of the output file. |
| results[].savedPercent | number | How much smaller the output is (negative if larger, which a format change can cause). |
| results[].quality | number | null | Quality actually used; `null` for PNG and lossless output. With `targetKb`, the quality the search settled on. |
| results[].lossless | boolean | Whether the output is lossless (always true for PNG). |
| results[].unchanged | boolean | Compressor only: no smaller version existed, so the original was returned. |
| results[].engine | "wasm" | "images" | What processed it: our in-Worker codecs, or Cloudflare Images for large photos. |
| results[].warnings | string[] | Anything worth knowing, e.g. transparency filled for JPEG, or only the first frame of a GIF converted. |
| results[].billableMegapixels | number | Units this image added to the charge. |
| summary.total | number | Images in the request. |
| summary.succeeded | number | Images converted. |
| summary.failed | number | Images that failed (not charged). |
| summary.inputBytes | number | Total input size of the successful images. |
| summary.outputBytes | number | Total output size. |
| summary.savedPercent | number | Overall saving. |
| summary.billableMegapixels | number | Total billable units; `meta.credits` = 1 + this, rounded up. |
| summary.engines | object | How many images each engine handled, e.g. `{ "wasm": 2 }`. |
| _meter | object | Billing meter (`{ unit: "megapixel", quantity }`) — the same number as `summary.billableMegapixels`. |
meta fields (every tool)
| Field | Type | Description |
|---|---|---|
| meta.tool | string | The tool id that ran. |
| meta.auth | string | How the call authenticated — `key` for an API key. |
| meta.credits | number | Everything this call cost, in credits (both currencies together). |
| meta.baseCredits | number | The tool's own price — the compute-credit part of `credits`. |
| meta.aiCredits | number | AI credits spent on a model step. Present only when non-zero. |
| meta.billedFrom | string | Which balance paid: the plan allowance or the purchased credit balance. |
| meta.pricing.mode | "flat" | "metered" | Flat price per call, or metered by usage. |
| meta.pricing.meterUnit | string | Metered tools: what is counted (e.g. `megapixel`). |
| meta.pricing.creditsPerUnit | number | Metered tools: credits per unit counted. |
| meta.quota.limit | number | Your plan's allowance for the current period. |
| meta.quota.used | number | Allowance used so far this period. |
| meta.quota.remaining | number | Allowance left this period. |
| meta.quota.resetsAt | string (ISO 8601) | When the allowance resets. |
| meta.quota.creditBalance | number | Purchased credits remaining, used after the allowance. |
| meta.quota.scope | string | What the quota belongs to — `account` (your workspace) for API keys. |
| meta.quota.plan | string | Your plan id. |
Ask for the file itself instead of JSON with ?output=binary on the URL (or an Accept: image/* header): one image comes back as the image, a batch as a ZIP containing every image plus results.json. The headers below carry the numbers. Errors are always JSON.
If every image in a call fails, the call returns 422 and is refunded.
Headers on a binary response
| Field | Type | Description |
|---|---|---|
| Content-Type | header | The image's MIME type, or `application/zip` for a batch. |
| Content-Disposition | header | Suggested file name, e.g. `photo.webp` or `images.zip`. |
| X-ToolsXpo-Credits | header | What the call cost. |
| X-ToolsXpo-Format | header | Single image: output format. |
| X-ToolsXpo-Width / -Height | header | Single image: output dimensions. |
| X-ToolsXpo-Input-Bytes / -Bytes | header | Single image: input and output size. |
| X-ToolsXpo-Saved-Percent | header | Single image: saving. |
| X-ToolsXpo-Quality | header | Single image: quality used (empty for lossless). |
| X-ToolsXpo-Engine | header | Single image: `wasm` or `images`. |
| X-ToolsXpo-Warnings | header | Single image: warnings, separated by ` | `. |
| X-ToolsXpo-Succeeded / -Failed | header | Batch: how many images succeeded and failed. Per-image detail is in `results.json` inside the ZIP. |
Errors
Failures use the same envelope with ok: false and a machine-readable code.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_input | A parameter failed validation. `details` lists which. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 | quota_exceeded | Out of credits on this workspace. |
| 404 | not_found | No such tool. |
| 429 | rate_limited | Too many requests. Retry after `Retry-After` seconds. |
| 503 | service_unavailable | Temporarily disabled by an administrator. |
How to use the Image Compressor API
Make images smaller — by quality, or down to a target file size — keeping each image's format unless you choose another. One image or up to ten per call.
Options
quality— 1–100, lower is smaller. Defaults: JPEG 75, WebP 75, AVIF 55. PNG is compressed losslessly.targetKb— aim for at most this many KB. The quality is lowered fromqualityuntil the file fits, and the result reports the quality it settled on. Counts 2× toward billing (it encodes several times).format—auto(default) keeps each image's format (GIF becomes PNG, HEIC becomes JPEG); or forcejpeg,png,webp,avif.maxWidth/maxHeight— shrink to fit first. Often the biggest saving of all.background— fill for transparency when the output is JPEG.
Three ways to send images
Upload files (multipart) — the way most code already sends a file. Repeat -F file=@… for up to 10 images; any other field is an option:
Bashcurl -X POST "https://tools.toolsxpo.com/v1/image-compressor?output=binary" \ -H "Authorization: Bearer $image-compressorSXPO_KEY" \ -F file=@photo.png -F targetKb=200 -o smaller.jpg
A raw body — the image bytes, options in the query string:
Bashcurl -X POST "https://tools.toolsxpo.com/v1/image-compressor?quality=70&output=binary" \ -H "Authorization: Bearer $image-compressorSXPO_KEY" -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg -o smaller.jpg
A URL or base64, as JSON — imageUrl (we fetch it, up to 20 MB) or image (base64 or a data: URL). For a batch, send images: [{ imageUrl }, { image }, …] — mix both freely:
JAVASCRIPTconst res = await fetch("https://tools.toolsxpo.com/v1/image-compressor", { method: "POST", headers: { Authorization: `Bearer ${process.env.image-compressorSXPO_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ images: [{ imageUrl: "https://example.com/a.jpg" }, { imageUrl: "https://example.com/b.png" }], targetKb: 200 }), }); const { data } = await res.json(); for (const r of data.results) if (r.ok) console.log(r.format, r.bytes, r.savedPercent);
A GET works too: ?imageUrl=…&imageUrl=…&OPTION=… (repeat imageUrl for a batch).
What comes back
- A download link (default):
data.results[]has one entry per image with aurlyou can download, embed or pass on, andexpiresAt— links last for your plan's file retention. Each entry also hasformat,contentType,width,height,inputBytes,bytes,savedPercent,quality,engineandwarnings. Failed images haveok: falseand anerror, and cost nothing.data.summarytotals the call. - Base64 instead: send
output: "base64"and each entry carries the image asbase64rather than a link — nothing is stored. - The file itself: add
?output=binaryto the URL (or sendAccept: image/*). One image comes back as the image; a batch as a ZIP with aresults.jsoninside. Credits, size and format are inX-ToolsXpo-*response headers. Errors are always JSON.
JSON{ "ok": true, "data": { "results": [ { "ok": true, "format": "webp", "width": 1600, "height": 1067, "bytes": 143210, "savedPercent": 82.4, "url": "https://ai-cdn.toolsxpo.com/…/9f2c….webp", "expiresAt": "2026-10-11T09:30:00.000Z" } ], "summary": { "succeeded": 1, "failed": 0 } }, "meta": { "credits": 3 } }
Links are unguessable but public: anyone holding one can open it until it expires. Use output: "base64" or ?output=binary for images that must not be stored.
Cost
A base price per call plus a charge per billable unit — both are shown in the price box on this page and returned in meta.credits with every response. An image's units are 0.2 × input megapixels + output megapixels × (3 for AVIF, else 1), at least 0.25 — so a typical 1–2 MP web image is about 2 units, and shrinking a big photo is cheap because the output is small. If every image in a call fails, the call is refunded.
Limits
- Up to 10 images per call, 20 MB each, 40 MB per call.
- Large photos (roughly over 3–4 megapixels) are processed by Cloudflare Images instead of our in-Worker codecs —
enginetells you which. You do not need to do anything; the cheapest engine that can handle the image is chosen automatically. - AVIF output is limited to about 1.5 MP in the Worker and 1,200 px per side on Cloudflare Images. For a large AVIF, pass
maxWidth. - HEIC and GIF input are accepted (GIF: first frame only). SVG, TIFF and BMP are not accepted by the API — convert those in the browser at toolsxpo.com.
- Metadata (EXIF, GPS) is removed; phone photos are rotated upright first.
In automations
Add the tool as a step. Its images are saved as files and the step output carries a url for each, so a later step can use {{previous_step.results.0.url}} — in an email, a webhook, or another image step's imageUrl. After a Source Files step, put {{previous_step}} in images to process every uploaded file.
Try it without code
Every field is available in the API Playground — including a file picker — and the web tool does the same thing in your browser for free, with nothing uploaded.
Never bigger than the original
If no smaller version exists at your settings — the image is already well compressed — the original is returned unchanged, with unchanged: true and a warning. You never get a "compressed" file that is larger.
Getting the most out of it
- Resize first. A 4000 px photo shown at 1200 px wastes ~90% of its bytes;
maxWidth=1600fixes that before quality matters. - For the smallest files, combine
format=webp(oravif) with a quality around 70.