API reference
Compress
POST /v1/compress makes an image, a PDF or an audio file smaller and returns it in the response.
Endpoint
Compresses one file and returns the result as the response body. Nothing is stored. The endpoint accepts images, PDFs and audio. Video must be sent as a job with operation: "compress".
If the file cannot be made smaller, your input is returned, marked Smol-Kept-Original: true, and the request is free. See Kept original.
Request
Authenticate with Authorization: Bearer $SMOL_API_KEY. Send the file in one of two forms. The file can be up to 25 MB (26,214,400 bytes), or 10 MB with a test key.
Raw body
The request body is the bytes of the file. Options go in the query string. Give the file's name with ?filename= or with a Content-Disposition request header. If both are present, the query parameter is used.
POST /v1/compress?image.format=avif&filename=photo.jpg HTTP/1.1
Host: api.smolmac.com
Authorization: Bearer $SMOL_API_KEY
Content-Length: 1614027
<the bytes of the file>- Any
Content-Typeother thanmultipart/form-datais treated as a raw body. The header's value is not used to detect the file type. - Send a
Content-Lengthheader. It is required with a test key. With a live key a body without it is accepted and streamed.
Multipart form
Send multipart/form-data with these parts:
| Part | Required | Content |
|---|---|---|
file | Yes | The file. Its name is taken from the part unless ?filename= is given. |
options | No | A JSON object of options, as text. It has the same shape as options on a job, for example {"image":{"format":"avif","quality":60}}. |
Options may also be given in the query string of a multipart request. Where an option appears in both places, the query string wins, field by field.
The file name
The name is optional for most files. It is used for two things: to tell apart formats that share a container or have no signature (see type detection), and to name the file that comes back. Only the last path component is used.
Options
In the query string, an option is written as its dotted path: ?image.quality=60. Booleans are true or false. Only the options for the kind of file you sent are used. The others are checked and then ignored, so the same options can be sent with any file. An unknown option is refused with invalid_options, and param names it.
| Option | Type | Description |
|---|---|---|
| image.format | string | original (default), jpg, png, webp, avif, heic, gif. See what original resolves to. |
| image.quality | integer | 0 to 100. Default 75. See the quality scale. |
| image.resize | boolean | Default true. false keeps the image's dimensions. Setting any of the three options below turns it on. |
| image.resize.mode | string | fit (default) shrinks the image to fit inside the box and never enlarges it. fill covers the box and crops to exactly its size. width shrinks to the given width. height shrinks to the given height. |
| image.resize.width | integer | Width of the box in pixels. Default 2000. Accepts 1 to 32768. Values outside 100 to 9999 are moved into that range. |
| image.resize.height | integer | Height of the box in pixels. Default 2000. The same range as the width. |
| image.strip_metadata | boolean | Default true. Removes camera, location and software details from the output. Not applied to PNG output: see Metadata. |
| pdf.quality | string | tiny, small, medium (default), large, original. See PDF presets. |
| audio.format | string | aac (default), m4a, mp3, ogg, opus, wav, flac, alac, original. |
| audio.quality | string | tiny, low, balanced, high (default), maximum. See audio tiers. |
| audio.channels | string | stereo (default), mono, original. mono mixes the audio down to one channel. The other two values keep the channels of the input. |
| audio.strip_metadata | boolean | Default false. true removes tags such as title and artist. |
| audio.bitrate_kbps | integer | 16 to 512. Replaces the bitrate of the quality tier. Not used by the lossless formats. |
| audio.sample_rate_hz | integer | 8000 to 192000. Default: the sample rate of the input. Opus output accepts only 8000, 12000, 16000, 24000 and 48000. MP3 accepts up to 48000. AAC accepts up to 96000. |
| preset | string | The name or id of a preset. Its options are applied first, and the options in the request override them field by field. |
| filename | string | Not an option: the name of the file. See "The file name" above. |
The video options are described in Options. They have no effect on this endpoint.
Response
Status 200. The body is the compressed file. The details are in the headers.
| Header | Type | Description |
|---|---|---|
| Content-Type | string | The media type of the returned file, for example image/avif. |
| Content-Length | integer | The size of the returned file in bytes. |
| Content-Disposition | string | attachment with a file name: the name you sent, with its extension replaced by the output format's. file is used when you sent no name. |
| Cache-Control | string | Always no-store. |
| Smol-Request-Id | string | The id of this request. Quote it to support. |
| Smol-Version | string | The version of the API that answered, for example 2026-10-01. |
| Smol-Original-Size | integer | Bytes received. |
| Smol-Output-Size | integer | Bytes returned. |
| Smol-Savings-Percent | number | The share of the original size that was saved, with one decimal. Negative when the output is larger. |
| Smol-Output-Format | string | The format of the returned file, for example avif. |
| Smol-Kept-Original | boolean | true when nothing smaller could be made and your input is returned. See Kept original. |
| Smol-Billed | boolean | true when the request was charged. Always false with a test key and for kept-original results. |
| Smol-Result | JSON | The full result object as compact JSON on one line. Characters outside printable ASCII are replaced by ?. |
| RateLimit-Limit | integer | The size of your rate limit bucket. |
| RateLimit-Remaining | integer | Requests left in the bucket. |
| RateLimit-Reset | integer | Seconds until the bucket is full again. See Limits. |
Errors
An error has a JSON body in place of the file. See Errors for the format and for what to do about each code.
| Code | Status | When |
|---|---|---|
missing_file | 400 | The body is empty, or the multipart form has no file part. |
invalid_options | 400 | An option is unknown or has a value that is not allowed. The options part is not valid JSON. |
bad_request | 400 | The multipart body cannot be parsed. A test key sent a raw body without Content-Length. The preset named does not exist. |
missing_key | 401 | No Authorization header. |
invalid_key | 401 | The key is not a valid key. |
revoked_key | 401 | The key has been revoked. |
no_payment_method | 402 | A live key on an account with no plan. |
payment_required | 402 | A live key on an account with an unpaid invoice. |
spend_cap_reached | 402 | The account has reached its monthly spend cap. |
test_mode_sample_required | 402 | A test key sent a file that is not a published sample. |
account_suspended | 403 | The account is suspended. |
ip_not_allowed | 403 | The key may not be used from this IP address. |
feature_not_enabled | 403 | The output would be HEIC, and HEIC output is not enabled for the account. This covers image.format=heic, a convert target of heic, and a HEIC file compressed with image.format left at original. |
file_too_large | 413 | The file is over 25 MB, or over 10 MB with a test key. |
unsupported_input | 415 | The file type is not recognised, or this endpoint does not handle it. |
use_async_job | 415 | The file is a video or an office document. These need a job. |
limit_exceeded | 422 | The file is over a pixel, page, frame or length limit. |
encrypted_pdf | 422 | The PDF needs a password to open. |
processing_failed | 422 | The file could not be processed. It may be damaged. |
rate_limited | 429 | Too many requests per second. |
concurrency_limited | 429 | Too many direct requests in progress at once. |
internal | 500 | Something went wrong on our side. |
engine_busy | 503 | The service is at capacity for a moment. |
timeout | 504 | Processing took longer than 120 seconds. |
Examples
Convert a JPEG to AVIF at quality 60, no wider than 1200 pixels:
curl -X POST "https://api.smolmac.com/v1/compress?image.format=avif&image.quality=60&image.resize.width=1200" \
-H "Authorization: Bearer $SMOL_API_KEY" \
--data-binary @photo.jpg \
-o photo.avifimport { readFile, writeFile } from "node:fs/promises";
const query = new URLSearchParams({
"image.format": "avif",
"image.quality": "60",
"image.resize.width": "1200",
filename: "photo.jpg",
});
const response = await fetch(`https://api.smolmac.com/v1/compress?${query}`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SMOL_API_KEY}` },
body: await readFile("photo.jpg"),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
const result = JSON.parse(response.headers.get("smol-result"));
await writeFile(`photo.${result.output_format}`, Buffer.from(await response.arrayBuffer()));
console.log(result.savings_percent, "% smaller, billed:", response.headers.get("smol-billed"));import json, os
import requests
with open("photo.jpg", "rb") as f:
response = requests.post(
"https://api.smolmac.com/v1/compress",
params={
"image.format": "avif",
"image.quality": 60,
"image.resize.width": 1200,
"filename": "photo.jpg",
},
headers={"Authorization": f"Bearer {os.environ['SMOL_API_KEY']}"},
data=f.read(),
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
result = json.loads(response.headers["Smol-Result"])
with open(f"photo.{result['output_format']}", "wb") as out:
out.write(response.content)
print(result["savings_percent"], "% smaller, billed:", response.headers["Smol-Billed"])The same request as a multipart form, with the options as JSON:
curl -X POST "https://api.smolmac.com/v1/compress" \
-H "Authorization: Bearer $SMOL_API_KEY" \
-F "[email protected]" \
-F 'options={"image":{"format":"avif","quality":60,"resize":{"width":1200}}}' \
-o photo.avifCompress a PDF with the smallest preset:
curl -X POST "https://api.smolmac.com/v1/compress?pdf.quality=tiny" \
-H "Authorization: Bearer $SMOL_API_KEY" \
--data-binary @report.pdf \
-o report.min.pdfExample response
HTTP/1.1 200 OK
Content-Type: image/avif
Content-Length: 188406
Content-Disposition: attachment; filename="photo.avif"; filename*=UTF-8''photo.avif
Cache-Control: no-store
Smol-Version: 2026-10-01
RateLimit-Limit: 40
RateLimit-Remaining: 39
RateLimit-Reset: 1
Smol-Request-Id: req_nawnA3JOXOxTy0iq0T57
Smol-Original-Size: 1614027
Smol-Output-Size: 188406
Smol-Savings-Percent: 88.3
Smol-Output-Format: avif
Smol-Kept-Original: false
Smol-Billed: true
Smol-Result: {"kind":"image","input_format":"jpg","output_format":"avif","original_size":1614027,"output_size":188406,"savings_percent":88.3,"kept_original":false,"increased":false,"width":1200,"height":800,"pages":null,"duration_seconds":null,"video_codec":null,"warnings":[]}
<the bytes of photo.avif>