Conversion API
Send an image, get it back converted, resized, watermarked or stripped of its metadata. Nothing is stored, the answer is the file itself.
Quick start
Grab your key
The same AUTH-Key the upload API uses, under Settings › Security in the dashboard.
POST the image
Multipart form data to /api/v1/convert.
Write the bytes
The body is the image. No JSON to unwrap.
Endpoint
https://imageserver.pw/api/v1/convert
multipart/form-data with the image in a part called
file. Authenticate with the
X-IMAGESERVER-AUTH-KEY header, or a
userKey body field for clients that cannot set headers.
Two limits apply: a daily allowance that depends on your plan, and 60 requests a minute
against loops.
https://imageserver.pw/api/v1/convert
Answers with the accepted values and their bounds as JSON, so a client can check them without reading this page. No key needed.
Request fields
| Field | Required | Description |
|---|---|---|
file |
yes | The image. JPEG, PNG, WebP, GIF, TIFF or AVIF, and it has to be one your plan allows. |
userKey |
no | Your user key as a body field. The header wins if both are present. |
format |
no |
keep (default), webp,
jpeg or png.
|
quality |
no |
40 to 100, default
82. For JPEG and WebP; PNG is lossless and ignores it.
|
maxEdge |
no |
64 to 10000.
Caps the longest edge and keeps the aspect ratio. Never enlarges a smaller picture.
|
strip |
no |
true by default. Removes camera and location data.
The EXIF rotation is applied first, so a phone photo does not come back on its side.
|
watermark |
no | Text, up to 60 characters. Sending nothing means no watermark. |
watermarkPosition |
no |
bottom-right (default), bottom-left,
top-right, top-left or
centre.
|
watermarkOpacity |
no | 5 to 100, default 65. |
watermarkSize |
no |
1 to 20, default
4. Its height as a percentage of the short edge.
|
A value outside its range is pulled to the nearest end rather than rejected, so a slightly wrong request still gives you a usable image.
Examples
curl -X POST https://imageserver.pw/api/v1/convert \
-H "X-IMAGESERVER-AUTH-KEY: your-user-key" \
-F "file=@/path/to/photo.png" \
-F "format=webp" \
-F "quality=82" \
-F "maxEdge=1920" \
-o photo.webp
$headers = @{ "X-IMAGESERVER-AUTH-KEY" = "your-user-key" }
$form = @{
file = Get-Item -Path "C:\path\to\photo.png"
format = "webp"
quality = "82"
maxEdge = "1920"
}
Invoke-WebRequest -Uri "https://imageserver.pw/api/v1/convert" `
-Method Post -Headers $headers -Form $form `
-OutFile "photo.webp"
import { readFile, writeFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('photo.png')]), 'photo.png');
form.append('format', 'webp');
form.append('maxEdge', '1920');
const res = await fetch('https://imageserver.pw/api/v1/convert', {
method: 'POST',
headers: { 'X-IMAGESERVER-AUTH-KEY': 'your-user-key' },
body: form,
});
if (!res.ok) throw new Error((await res.json()).error);
console.log('applied:', res.headers.get('X-Image-Applied'));
console.log('left today:', res.headers.get('RateLimit-Remaining'));
await writeFile('photo.webp', Buffer.from(await res.arrayBuffer()));
import requests
with open("photo.png", "rb") as handle:
response = requests.post(
"https://imageserver.pw/api/v1/convert",
headers={"X-IMAGESERVER-AUTH-KEY": "your-user-key"},
files={"file": ("photo.png", handle)},
data={"format": "webp", "quality": "82", "maxEdge": "1920"},
)
response.raise_for_status()
print("applied:", response.headers["X-Image-Applied"])
with open("photo.webp", "wb") as out:
out.write(response.content)
Response
On success the body is the image, with the content type of whatever came out. Nothing to unwrap, and nothing kept on our side. A successful answer, and the refusal for an exhausted day, carry these:
| Header | Meaning |
|---|---|
X-Image-Applied |
What actually happened, for example resized to 1920px, metadata removed, converted to WebP.
Metadata is removed unless you send strip=false, so a request
that asks for nothing else still reads metadata removed.
It reads none only when nothing was changed.
|
RateLimit-Limit |
Conversions your plan allows per day. |
RateLimit-Remaining |
How many are left. Read this rather than counting yourself. |
RateLimit-Reset |
Seconds until the allowance resets, which is midnight UTC. |
Refusals that happen before any work, the 400 to 413 below and the 429 for more than 60
requests a minute, carry the same three RateLimit-* headers
counting that per-minute limit instead. Read them as the day's allowance only on a
successful answer.
Errors
| Status | Message | What to do |
|---|---|---|
400 |
No file was sent. | The part has to be called file. |
400 |
File content does not match its extension. | The bytes are checked as well as the name. Send the file under its real extension. |
401 |
Missing or invalid user key. | Copy the key again from the dashboard, it may have been regenerated. |
402 |
Not included in your plan. | The body carries an upgradeUrl. See plans. |
403 |
Your account is suspended. | Contact us on Discord. |
413 |
File exceeds your plan limit of X. | The message names your actual limit. Shrink the file or upgrade. |
422 |
Nothing was done, with the reason. | Usually animated. See the note below. |
429 |
Too fast, or out of allowance for the day. | Read RateLimit-Reset and wait that many seconds. Daily conversion limit of N reached. means the day is used up and comes with resetsAt; Too many requests. Please slow down. is the per-minute limit. |
A request refused before any work happens, for a missing file, the wrong type or a size over your limit, does not cost you part of the day's allowance.
An animation is refused, not flattened
Converting an animated GIF or WebP the obvious way returns its first frame and nothing
else. Rather than hand back a picture where a clip was expected, the endpoint answers
422 with "reason": "animated"
and leaves your file alone.
Just the one image?
The browser tools do the same conversions without a key, without a quota and without the file ever leaving your machine.