Quick start
- Sign in with your email and copy your API key from the dashboard.
- Run the command below with your key and any PNG, JPEG or WebP file.
- photo-min.png is the compressed copy. That is the whole integration.
curl https://api.compresso.space/v1/compress \
--user api:YOUR_API_KEY \
--data-binary @photo.png \
--output photo-min.png \
--fail-with-bodyIf something goes wrong, curl exits with an error and photo-min.png contains a short JSON message that says what happened and how to fix it.
On Windows PowerShell, type curl.exe instead of curl: plain curl there is an alias for a different command.
Authentication
Every request needs your API key. Send it with HTTP Basic auth, using any user name and the key as the password (curl --user api:YOUR_API_KEY), or in an Authorization: Bearer header. Both work everywhere.
Requests must use HTTPS. Plain HTTP is refused with https_required, never redirected. If a key was ever sent over plain HTTP, revoke it in your dashboard and create a new one.
Keep keys on the server. The API refuses requests that come from a web page in a browser (browser_not_supported), so a key can never be exposed to your visitors. You can have up to 10 active keys and revoke any of them at any time.
curl https://api.compresso.space/v1/usage --user api:YOUR_API_KEYCompressing images
POST the image to /v1/compress. Send the raw file bytes as the request body, or a multipart/form-data upload with the image in a field named file. The format is detected from the file itself, so the Content-Type of a raw upload does not matter.
A successful response is the compressed image itself, with status 200 and the same format you sent. Save the body as a binary file. There is no second download step.
# raw bytes
curl https://api.compresso.space/v1/compress \
--user api:YOUR_API_KEY \
--data-binary @photo.png \
--output photo-min.png \
--fail-with-body
# or as a form upload (field "file")
curl https://api.compresso.space/v1/compress \
--user api:YOUR_API_KEY \
--form file=@photo.png \
--output photo-min.png \
--fail-with-bodyResponse headers
| Content-Type | image/png, image/jpeg or image/webp, the same as the input. |
|---|---|
| Original-Size | Size of your upload in bytes. |
| Compressed-Size | Size of the returned image in bytes. |
| Compression-Count | Compressions used by your account today (UTC), including this one. |
| Compression-Limit | Your daily limit. |
| Compression-Reset | When the count resets: the next 00:00 UTC, for example 2026-09-19T00:00:00Z. |
| Request-Id | Quote it when you contact us about a specific request. |
Checking your usage
GET /v1/usage returns your account's usage without compressing anything. It is a handy way to check that a key works. It does not count towards the limit.
curl https://api.compresso.space/v1/usage --user api:YOUR_API_KEY{
"ok": true,
"plan": "free",
"compression_count": 3,
"compression_limit": 100,
"compression_reset": "2026-09-19T00:00:00Z",
"key": "csk_…a1b2"
}Compressing a folder
Send images one at a time: each account compresses one image at a time with one more waiting, so parallel uploads only get rate_limited. When the API answers 429 or 503 with a Retry-After header, wait that many seconds and try again. Without Retry-After, do not retry.
// Compress every PNG/JPEG/WebP in ./images into ./images-min, one at a time.
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"
import path from "node:path"
const KEY = process.env.COMPRESSO_API_KEY
const IN = "images"
const OUT = "images-min"
await mkdir(OUT, { recursive: true })
for (const name of await readdir(IN)) {
if (!/\.(png|jpe?g|webp)$/i.test(name)) continue
const body = await readFile(path.join(IN, name))
for (;;) {
const res = await fetch("https://api.compresso.space/v1/compress", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}` },
body,
})
if (res.ok) {
await writeFile(path.join(OUT, name), Buffer.from(await res.arrayBuffer()))
console.log(name, res.headers.get("Original-Size"), "->", res.headers.get("Compressed-Size"))
break
}
const err = await res.json()
// Retry only when the API says so, and only for short waits.
const wait = Number(res.headers.get("Retry-After"))
if (wait > 0 && wait <= 60) {
await new Promise((r) => setTimeout(r, wait * 1000))
continue
}
console.error(name, err.error, err.message)
if (res.status === 429 || res.status === 503) process.exit(1) // limit or long outage
break
}
}What happens to your image
- PNG, JPEG and WebP are compressed with the same engine and the same per-pixel quality gate as compresso.space. The result stays visually identical to the original.
- The format and the pixel dimensions never change. Nothing is resized or converted.
- Compressed images carry no metadata (EXIF, GPS, camera data). Photos are rotated according to their EXIF orientation first, so they stay upright.
- If an image cannot get smaller without a visible difference, you get the original file back unchanged, metadata included, with status 200. It still counts as a compression.
- Images are processed in memory and never written to disk or stored. The image itself is never logged: we keep only sizes and the format for statistics.
Common mistakes
- curl -d mangles binary files. Always use --data-binary @file or --form file=@file.
- Do not send base64, a data: URL or JSON with an image link. Send the file bytes.
- axios needs responseType: "arraybuffer", otherwise the image is decoded as text and corrupted.
- In Python requests, write res.content (bytes), never res.text.
- Call the API from your server or build scripts, not from front-end JavaScript: browsers are refused on purpose.
Limits
| Free compressions | 100 per account per day, reset at 00:00 UTC |
|---|---|
| File size | up to 16 MB per image |
| Resolution | up to 16 megapixels |
| Parallel requests | 1 in progress and 1 waiting per account |
| Request rate | 60 requests per minute per account |
| Per network | 500 compressions per day per IP address, across all keys |
| API keys | up to 10 active keys per account |
| Timeout | set your client timeout to at least 120 seconds |
Errors
Every error is JSON: {"ok": false, "error": "code", "message": "...", "docs": "...", "request_id": "..."}. The message explains what happened and what to do, so print it.
Retry rule: if a 429 or 503 carries a Retry-After header, wait that many seconds and retry. If there is no Retry-After, do not retry automatically.
| Code | HTTP | What it means | Retry? |
|---|---|---|---|
| https_required | 400 | The request used plain HTTP. Use https://api.compresso.space/v1. | No |
| input_missing | 400 | The request body was empty. Send the image bytes. | No |
| invalid_multipart | 400 | The form upload is malformed. Let your HTTP client build it, or send raw bytes. | No |
| invalid_image | 400 | The file is truncated or corrupted. With curl, use --data-binary, not -d. | No |
| unauthorized | 401 | No key, a malformed or truncated key, an unknown key, or a revoked key. The message says which. | No |
| browser_not_supported | 403 | The request came from a web page in a browser. Call the API from a server. | No |
| not_found | 404 | No such endpoint. The API has POST /v1/compress, GET /v1/usage and GET /v1/openapi.json. | No |
| method_not_allowed | 405 | Wrong HTTP method for this endpoint. The Allow header lists the right one. | No |
| file_too_large | 413 | The file is larger than 16 MB. | No |
| image_too_large | 413 | The image is larger than 16 megapixels. It is never downscaled silently. | No |
| unsupported_format | 415 | Not a PNG, JPEG or WebP. The message names the format that was detected. | No |
| animated_unsupported | 415 | Animated images are not supported. | No |
| daily_limit_exceeded | 429 | Your daily limit is used up. It resets at 00:00 UTC, see Compression-Reset. | No |
| rate_limited | 429 | Too many requests at once or per minute. Wait Retry-After seconds. | Yes, after Retry-After |
| server_busy | 503 | The server is busy with other work. Wait Retry-After seconds. | Yes, after Retry-After |
| daily_capacity_reached | 503 | The API's shared daily capacity is used up. It resets at 00:00 UTC. | Yes, after Retry-After |
| api_disabled | 503 | The API is temporarily switched off for maintenance. Try again later. | Later |
| compress_failed | 500 | An unexpected error on our side. Retry once, then contact us with the request_id. | Once |
OpenAPI
The full machine-readable description of the API is at https://api.compresso.space/v1/openapi.json (OpenAPI 3.1, no key needed). Import it into Postman or Insomnia, or generate a client from it.
Changelog
- API v1 released: /v1/compress, /v1/usage, 100 free compressions a day.