ToolsXpo
ToolsXpo

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:

EndpointDoes
POST https://tools.toolsxpo.com/v1/image-converterChanges the format: WebP, AVIF, JPEG, PNG — with quality, lossless and resize options.
POST https://tools.toolsxpo.com/v1/image-compressorMakes 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

Bash
curl -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

Bash
curl -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

Bash
curl -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+)

JAVASCRIPT
import { 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

PYTHON
import 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:

Bash
curl -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)

FieldTypeDescription
imagestringThe image as base64, or a data:image/…;base64,… URL. JPEG, PNG, WebP, AVIF, GIF or HEIC. Max 20 MB decoded.
imageUrlstringPublic http(s) URL we fetch the image from. Max 20 MB, 15 s, up to 3 redirects.
namestringLabel echoed back in the result (e.g. the file name). Max 200 characters.
imagesarray, 1–10Batch: [{ "image": "…" } | { "imageUrl": "…" }, …], each with an optional name. Also accepts a list of URLs. The same options apply to every image.
file uploadmultipart file-F file=@photo.jpg — any file field, repeat for up to 10.

image-converter options

FieldTypeDefaultDescription
formatjpeg | png | webp | avif— requiredOutput format. jpg is accepted as jpeg.
qualityinteger 1–100JPEG 82, WebP 80, AVIF 60Lower is smaller. Ignored for PNG and lossless output.
losslessbooleanfalseExact-pixel WebP or AVIF.
maxWidthinteger 1–12000—Shrink to at most this width, keeping the aspect ratio. Never enlarges.
maxHeightinteger 1–12000—Shrink to at most this height.
backgroundhex colour#ffffffFill for transparent areas when the output is JPEG.
outputurl | base64urlHow images come back in JSON: a download link, or inline base64.

image-compressor options

FieldTypeDefaultDescription
qualityinteger 1–100JPEG 75, WebP 75, AVIF 55Lower is smaller. PNG is compressed losslessly.
targetKbinteger 1–20480—Aim for at most this many KB; the quality is searched downward from quality.
formatauto | jpeg | png | webp | avifautoauto keeps each image's format (GIF → PNG, HEIC → JPEG).
maxWidth / maxHeightinteger 1–12000—Shrink to fit first. Never enlarges.
backgroundhex colour#ffffffFill for transparency when the output is JPEG.
outputurl | base64urlDownload link or inline base64.

Query-string only

ParameterDescription
output=binaryReturn the file itself instead of JSON — the image, or a ZIP for a batch. Same as sending Accept: image/*.
api_tokenYour API key, if you cannot send the Authorization header. Prefer the header.

Response reference

Each entry in data.results

FieldTypeDescription
indexnumberPosition of the image in the request (0-based).
namestring | nullThe name you sent, or the uploaded file name.
okbooleanfalse if this image failed. Failed images cost nothing; the rest still succeed.
errorstringWhy it failed — only when ok is false.
urlstringDownload link (default). Public but unguessable.
expiresAtstring (ISO 8601)When url stops working — your plan's file retention (1–20 days).
base64stringThe image inline — when you sent output: "base64".
formatstringOutput format: jpeg, png, webp or avif.
contentTypestringMIME type, e.g. image/webp.
width, heightnumberOutput dimensions in pixels.
inputFormatstringDetected input format (jpeg, png, webp, avif, gif, heic…).
inputWidth, inputHeightnumberInput dimensions (after rotating phone photos upright).
inputBytes, bytesnumberInput and output file sizes.
savedPercentnumberHow much smaller the output is (negative if larger).
qualitynumber | nullQuality used — with targetKb, the one the search settled on. null for PNG/lossless.
losslessbooleanWhether the output is lossless.
unchangedbooleanCompressor: nothing smaller existed, so the original was returned.
enginewasm | imagesWhat processed it.
warningsstring[]e.g. transparency filled for JPEG, only the first GIF frame converted.
billableMegapixelsnumberUnits this image added to the charge.

data.summary

FieldDescription
total, succeeded, failedImage counts.
inputBytes, outputBytes, savedPercentTotals for the successful images.
billableMegapixelsTotal units; meta.credits = 1 + this, rounded up.
enginesHow many images each engine handled, e.g. { "wasm": 2 }.

meta (every tool)

FieldDescription
creditsWhat the call cost.
baseCredits, aiCreditsThe compute and AI parts of credits (aiCredits only when non-zero).
billedFromWhich balance paid: the plan allowance or purchased credits.
pricing{ mode: "metered", meterUnit: "megapixel", creditsPerUnit: 1 }
quotalimit, used, remaining, resetsAt, creditBalance, plan — your standing after this call.

Binary responses (?output=binary)

HeaderDescription
Content-Type, Content-DispositionThe image's type and a suggested file name — or application/zip / images.zip for a batch.
X-ToolsXpo-CreditsWhat the call cost.
X-ToolsXpo-Format, -Width, -HeightSingle image: output format and size.
X-ToolsXpo-Input-Bytes, -Bytes, -Saved-PercentSingle image: sizes and saving.
X-ToolsXpo-Quality, -Engine, -WarningsSingle image: quality used, engine, warnings (|-separated).
X-ToolsXpo-Succeeded, -FailedBatch: counts. The ZIP's results.json holds every entry's fields as above.

Errors

Statuserror.codeWhen
400invalid_inputA field failed validation (error.details lists which), more than 10 images, or no image source.
401unauthorizedMissing or invalid API key.
402 / 429payment_required / quota_exceededOut of credits.
413invalid_inputUploads over 40 MB in one call.
415invalid_inputA file uploaded to a tool that does not take images.
422unprocessableEvery image in the call failed — the call is refunded.
429rate_limitedToo 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. engine in the result says which.
  • AVIF output is limited to roughly 1.5 MP (1,200 px per side for very large photos) — pass maxWidth for 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

Image Converter

Image & SVG

Image Compressor

Image & SVG

imageswebpavifcompressconvertuploadbatch

Related