Convert and compress images with the API
Convert between JPEG, PNG, WebP and AVIF, compress to a quality or a target size, and resize — by file upload, URL or base64, up to ten images per call.
Updated October 4, 2026
Two endpoints cover almost everything you do to an image before putting it on the web:
| Endpoint | Does |
|---|---|
POST https://tools.toolsxpo.com/v1/image-converter | Changes the format: WebP, AVIF, JPEG, PNG — with quality, lossless and resize options. |
POST https://tools.toolsxpo.com/v1/image-compressor | Makes it smaller: by quality or down to a target size in KB, keeping the format unless you pick one. Never returns a larger file. |
Both take one image or up to ten in a call, and accept the same three kinds of input.
Send images the way you already have them
1. Upload files
Bashcurl -X POST "https://tools.toolsxpo.com/v1/image-converter?output=binary" \ -H "Authorization: Bearer $TOOLSXPO_KEY" \ -F file=@photo.png -F format=webp -F quality=80 \ -o photo.webp
Repeat -F file=@… for a batch. Every other form field is an option.
2. Send the bytes as the body
Bashcurl -X POST "https://tools.toolsxpo.com/v1/image-compressor?targetKb=150&output=binary" \ -H "Authorization: Bearer $TOOLSXPO_KEY" \ -H "Content-Type: image/jpeg" --data-binary @photo.jpg \ -o smaller.jpg
3. Point at a URL, or send base64
Bashcurl -X POST "https://tools.toolsxpo.com/v1/image-converter" \ -H "Authorization: Bearer $TOOLSXPO_KEY" -H "Content-Type: application/json" \ -d '{"format":"avif","maxWidth":1200,"images":[{"imageUrl":"https://example.com/a.jpg"},{"imageUrl":"https://example.com/b.png"}]}'
image takes base64 or a data: URL; imageUrl takes any public http(s) URL up to 20 MB. A GET works too — ?imageUrl=…&imageUrl=…&format=webp.
In your code
JavaScript (Node 20+)
JAVASCRIPTimport { openAsBlob } from "node:fs"; import { writeFile } from "node:fs/promises"; const form = new FormData(); form.append("file", await openAsBlob("photo.jpg"), "photo.jpg"); form.append("format", "webp"); form.append("quality", "80"); const res = await fetch("https://tools.toolsxpo.com/v1/image-converter?output=binary", { method: "POST", headers: { Authorization: `Bearer ${process.env.TOOLSXPO_KEY}` }, body: form, }); if (!res.ok) throw new Error((await res.json()).error.message); await writeFile("photo.webp", Buffer.from(await res.arrayBuffer())); console.log("credits", res.headers.get("x-toolsxpo-credits"), "saved", res.headers.get("x-toolsxpo-saved-percent") + "%");
Python
PYTHONimport os, requests res = requests.post( "https://tools.toolsxpo.com/v1/image-compressor", headers={"Authorization": f"Bearer {os.environ['TOOLSXPO_KEY']}"}, files=[("file", open("a.jpg", "rb")), ("file", open("b.png", "rb"))], data={"quality": "70", "maxWidth": "1600"}, ) for r in res.json()["data"]["results"]: print(r["name"], r.get("url"), r.get("savedPercent"), r.get("error"))
What you get back
JSON with a download link, by default. data.results has one entry per image with a url — download it, put it in an <img>, store it, or hand it to the next system — and expiresAt: links are kept for your plan's file retention. Each entry also has format, width, height, inputBytes, bytes, savedPercent, quality, engine and warnings. An image that failed has ok: false and an error; the others still succeed. data.summary totals the call.
JSON{ "ok": true, "data": { "results": [ { "ok": true, "format": "webp", "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 } }
Download it like any file:
Bashcurl -s -X POST "https://tools.toolsxpo.com/v1/image-converter" -H "Authorization: Bearer $TOOLSXPO_KEY" \ -H "Content-Type: application/json" -d '{"imageUrl":"https://example.com/a.jpg","format":"webp"}' \ | jq -r '.data.results[0].url' | xargs curl -s -o a.webp
Inline instead: send "output": "base64" and each entry carries base64 rather than a link; nothing is stored. Links are unguessable but public until they expire, so use this (or the binary response) for images that must not be stored.
The file itself with ?output=binary or Accept: image/*: a single image comes back as the image, a batch as a ZIP with a results.json describing each one. The numbers are in X-ToolsXpo-Credits, -Width, -Height, -Format, -Bytes, -Saved-Percent, -Quality and -Engine headers. Errors are always JSON.
Parameter reference
Every request needs one image source: image, imageUrl, an uploaded file, or images for a batch — never both a single source and images. All other fields are optional except the converter's format. The same names work as JSON fields, query parameters and form fields.
Image source (both endpoints)
| Field | Type | Description |
|---|---|---|
image | string | The image as base64, or a data:image/…;base64,… URL. JPEG, PNG, WebP, AVIF, GIF or HEIC. Max 20 MB decoded. |
imageUrl | string | Public http(s) URL we fetch the image from. Max 20 MB, 15 s, up to 3 redirects. |
name | string | Label echoed back in the result (e.g. the file name). Max 200 characters. |
images | array, 1–10 | Batch: [{ "image": "…" } | { "imageUrl": "…" }, …], each with an optional name. Also accepts a list of URLs. The same options apply to every image. |
| file upload | multipart file | -F file=@photo.jpg — any file field, repeat for up to 10. |
image-converter options
| Field | Type | Default | Description |
|---|---|---|---|
format | jpeg | png | webp | avif | — required | Output format. jpg is accepted as jpeg. |
quality | integer 1–100 | JPEG 82, WebP 80, AVIF 60 | Lower is smaller. Ignored for PNG and lossless output. |
lossless | boolean | false | Exact-pixel WebP or AVIF. |
maxWidth | integer 1–12000 | — | Shrink to at most this width, keeping the aspect ratio. Never enlarges. |
maxHeight | integer 1–12000 | — | Shrink to at most this height. |
background | hex colour | #ffffff | Fill for transparent areas when the output is JPEG. |
output | url | base64 | url | How images come back in JSON: a download link, or inline base64. |
image-compressor options
| Field | Type | Default | Description |
|---|---|---|---|
quality | integer 1–100 | JPEG 75, WebP 75, AVIF 55 | Lower is smaller. PNG is compressed losslessly. |
targetKb | integer 1–20480 | — | Aim for at most this many KB; the quality is searched downward from quality. |
format | auto | jpeg | png | webp | avif | auto | auto keeps each image's format (GIF → PNG, HEIC → JPEG). |
maxWidth / maxHeight | integer 1–12000 | — | Shrink to fit first. Never enlarges. |
background | hex colour | #ffffff | Fill for transparency when the output is JPEG. |
output | url | base64 | url | Download link or inline base64. |
Query-string only
| Parameter | Description |
|---|---|
output=binary | Return the file itself instead of JSON — the image, or a ZIP for a batch. Same as sending Accept: image/*. |
api_token | Your API key, if you cannot send the Authorization header. Prefer the header. |
Response reference
Each entry in data.results
| Field | Type | Description |
|---|---|---|
index | number | Position of the image in the request (0-based). |
name | string | null | The name you sent, or the uploaded file name. |
ok | boolean | false if this image failed. Failed images cost nothing; the rest still succeed. |
error | string | Why it failed — only when ok is false. |
url | string | Download link (default). Public but unguessable. |
expiresAt | string (ISO 8601) | When url stops working — your plan's file retention (1–20 days). |
base64 | string | The image inline — when you sent output: "base64". |
format | string | Output format: jpeg, png, webp or avif. |
contentType | string | MIME type, e.g. image/webp. |
width, height | number | Output dimensions in pixels. |
inputFormat | string | Detected input format (jpeg, png, webp, avif, gif, heic…). |
inputWidth, inputHeight | number | Input dimensions (after rotating phone photos upright). |
inputBytes, bytes | number | Input and output file sizes. |
savedPercent | number | How much smaller the output is (negative if larger). |
quality | number | null | Quality used — with targetKb, the one the search settled on. null for PNG/lossless. |
lossless | boolean | Whether the output is lossless. |
unchanged | boolean | Compressor: nothing smaller existed, so the original was returned. |
engine | wasm | images | What processed it. |
warnings | string[] | e.g. transparency filled for JPEG, only the first GIF frame converted. |
billableMegapixels | number | Units this image added to the charge. |
data.summary
| Field | Description |
|---|---|
total, succeeded, failed | Image counts. |
inputBytes, outputBytes, savedPercent | Totals for the successful images. |
billableMegapixels | Total units; meta.credits = 1 + this, rounded up. |
engines | How many images each engine handled, e.g. { "wasm": 2 }. |
meta (every tool)
| Field | Description |
|---|---|
credits | What the call cost. |
baseCredits, aiCredits | The compute and AI parts of credits (aiCredits only when non-zero). |
billedFrom | Which balance paid: the plan allowance or purchased credits. |
pricing | { mode: "metered", meterUnit: "megapixel", creditsPerUnit: 1 } |
quota | limit, used, remaining, resetsAt, creditBalance, plan — your standing after this call. |
Binary responses (?output=binary)
| Header | Description |
|---|---|
Content-Type, Content-Disposition | The image's type and a suggested file name — or application/zip / images.zip for a batch. |
X-ToolsXpo-Credits | What the call cost. |
X-ToolsXpo-Format, -Width, -Height | Single image: output format and size. |
X-ToolsXpo-Input-Bytes, -Bytes, -Saved-Percent | Single image: sizes and saving. |
X-ToolsXpo-Quality, -Engine, -Warnings | Single image: quality used, engine, warnings (|-separated). |
X-ToolsXpo-Succeeded, -Failed | Batch: counts. The ZIP's results.json holds every entry's fields as above. |
Errors
| Status | error.code | When |
|---|---|---|
| 400 | invalid_input | A field failed validation (error.details lists which), more than 10 images, or no image source. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 / 429 | payment_required / quota_exceeded | Out of credits. |
| 413 | invalid_input | Uploads over 40 MB in one call. |
| 415 | invalid_input | A file uploaded to a tool that does not take images. |
| 422 | unprocessable | Every image in the call failed — the call is refunded. |
| 429 | rate_limited | Too many requests; see Retry-After. |
Cost
1 credit per call, plus 1 per billable unit. An image's units are 0.2 × input megapixels + output megapixels × (3 for AVIF, else 1), at least 0.25; a targetKb search counts double. A typical 1–2 MP web image costs 2–3 credits. Failed images cost nothing, and if every image in a call fails the call is refunded.
Limits and behaviour
- 10 images per call, 20 MB per image, 40 MB per call.
- The engine is chosen per image by cost: our in-Worker codecs for typical web images, Cloudflare Images for large photos.
enginein the result says which. - AVIF output is limited to roughly 1.5 MP (1,200 px per side for very large photos) — pass
maxWidthfor a big AVIF. - Accepted input: JPEG, PNG, WebP, AVIF, GIF (first frame) and HEIC. SVG, TIFF and BMP are not accepted by the API.
- EXIF and GPS metadata are removed; phone photos are rotated upright first.
No code?
The Image Converter and Image Compressor pages do the same in your browser — free, nothing uploaded. And every field, including a file picker, is in the API Playground.
Tools covered here
Related
Process images in automations
Convert, compress and resize images on a schedule or a trigger: feed in uploaded files or URLs, and pass the results on as links to email, webhooks or other steps.
AI models — one OpenAI-compatible endpoint
Call every model we host from any OpenAI client by changing one string. Streaming, bring-your-own-key, scheduling and tool chaining.
AES Encryption API
Call the AES Encryption API — GET /v1/aes-encryption. Parameters, example requests in cURL, JavaScript and Python, credits per call, and the JSON response.
AI Text Embeddings API
Call the AI Text Embeddings API — GET /v1/text-embeddings. Parameters, example requests in cURL, JavaScript and Python, credits per call, and the JSON response.