I'm Oleksiy, a solo developer from Kyiv. I run CleverUtils, a free online file converter. People kept asking whether they could use it from scripts instead of the browser, so there is now a public REST API. This post shows how I use it from a terminal: one-off conversions with curl, a small Python script with polling and rate-limit handling, and batch jobs. I also cover the limits and the cases where you should skip the API and run FFmpeg or ImageMagick yourself.

Everything below comes from the CleverUtils API docs. If something here disagrees with the docs, trust the docs.

The short version

  • Base URL: https://cleverutils.com/api/v1
  • No signup and no API key. Rate limits are per IP address.
  • Upload limit: 200 MB per file (the website allows 100 MB).
  • Converted files are deleted 2 hours after conversion, so download them right away.
  • Responses are JSON with a data envelope.
  • CORS is open (Access-Control-Allow-Origin: *), so it also works from a browser page.

Your first conversion with curl

Converting an iPhone HEIC photo to JPG is one multipart POST. You send the file and a to_format value:

curl -X POST https://cleverutils.com/api/v1/convert \ -F "file=@photo.heic" \ -F "to_format=jpg" 

The response looks like this:

{ "data": { "job_id": "5f8c1a2e9d4b7c0e3f6a8b2d1e4c5f78", "status": "done", "output": { "filename": "photo_cleverutils.com.jpg", "size_bytes": 184523, "size_human": "180 KB", "url": "https://cleverutils.com/api/v1/jobs/5f8c.../output" }, "links": { "self": "https://cleverutils.com/api/v1/jobs/5f8c...", "output": "https://cleverutils.com/api/v1/jobs/5f8c.../output" }, "expires_at": "2026-04-10T14:32:11Z" } } 

Then download the result:

curl -o photo.jpg https://cleverutils.com/api/v1/jobs/<job_id>/output 

With jq you can chain both steps into one line:

url=$(curl -s -X POST https://cleverutils.com/api/v1/convert \ -F "file=@photo.heic" -F "to_format=jpg" | jq -r '.data.output.url') curl -o photo.jpg "$url" 

to_format is just the target extension: png, mp3, pdf, webp and so on. The same endpoint handles images, audio, video, documents, archives, fonts and 3D formats.

Useful optional parameters

/api/v1/convert takes a few optional fields. The ones I use most:

Field What it does
img_quality 0-100, default 80. Used for JPG, WebP and PDF compression
img_resize_w, img_resize_h Resize to exact dimensions (16-7680 px), one or both
img_rotate 90, 180, 270, flip-h, flip-v
img_crop_x/y/w/h Crop region in pixels
mp3_mode, mp3_quality, mp3_bitrate MP3 encoder options
wav_rate, wav_depth, wav_channels WAV output options
ocr_lang Tesseract language for OCR, default eng

For example, to make a smaller WebP thumbnail:

curl -X POST https://cleverutils.com/api/v1/convert \ -F "file=@banner.png" \ -F "to_format=webp" \ -F "img_quality=70" \ -F "img_resize_w=800" 

Status, polling and errors

Small files usually come back with "status": "done". Bigger jobs such as long videos can return "processing", sometimes with a progress value from 0 to 99. You poll:

curl https://cleverutils.com/api/v1/jobs/<job_id> 

According to the reference, a job ends up in one of these states:

  • status: "processing": still running
  • status: "done": the output block is filled in
  • HTTP 404 with error.code: JOB_NOT_FOUND: wrong ID or the job expired
  • HTTP 500 with error.code: CONVERSION_FAILED: the conversion crashed

A Python script I actually use

The quickstart's Python example is four lines with requests. The version below adds polling, a download step and basic 429 handling:

import sys import time import requests API = "https://cleverutils.com/api/v1" def convert(path, to_format, **options): with open(path, "rb") as f: resp = requests.post( f"{API}/convert", files={"file": f}, data={"to_format": to_format, **options}, ) if resp.status_code == 429: wait = int(resp.headers.get("Retry-After", "1")) time.sleep(wait) return convert(path, to_format, **options) resp.raise_for_status() job = resp.json()["data"] while job["status"] == "processing": time.sleep(2) job = requests.get(f"{API}/jobs/{job['job_id']}").json()["data"] out = requests.get(job["output"]["url"]) out.raise_for_status() with open(job["output"]["filename"], "wb") as f: f.write(out.content) return job["output"]["filename"] if __name__ == "__main__": print(convert(sys.argv[1], sys.argv[2])) 

Usage: python convert.py report.docx pdf.

Two notes. Polling every 2 seconds stays well clear of the 1-second cooldown. For production code, put a retry cap on that 429 branch so it can't loop forever.

Batch: up to 20 files per request

For a folder of photos, POST /api/v1/batch accepts up to 20 files in one multipart request, all converted to the same to_format:

curl -X POST https://cleverutils.com/api/v1/batch \ -F "files[]=@IMG_0001.heic" \ -F "files[]=@IMG_0002.heic" \ -F "files[]=@IMG_0003.heic" \ -F "to_format=jpg" 

You get back a batch_id (format bat_ plus 16 hex characters) and an array of per-file jobs. Poll the whole batch with GET /api/v1/batches/{batch_id}. It returns overall_status (done, processing or failed) plus each job, and you download each file from its job's output URL. A batch saves round trips, but every file still counts against your daily quota.

Tools that aren't a format swap

POST /api/v1/tools/{slug} covers jobs that don't fit "A to B". A few examples from the reference: compress-pdf, merge-pdf (send files[]), split-pdf, extract-audio-from-video, trim-video, resize-image, and AI tools like remove-background, upscale-image and speech-to-text (options format = txt/srt/vtt, quality, language). Each tool has its own parameters, so check the reference before you use one.

Rate limits, honestly

There's no key, so limits are per IP. The defaults from the rate-limits page:

  • 1-second cooldown between requests
  • 20 requests per rolling 60 seconds
  • 150 per hour, 1000 per day
  • AI tools: 50 or 100 per day depending on the tool. upscale-image also has a 20 MB internal limit

Every response includes X-RateLimit-Remaining-Day, X-RateLimit-Remaining-Hour and related headers. GET /api/v1/limits shows your usage without counting against the quota, and GET /api/v1/health is a free liveness check. If you go over a limit you get a 429 with Retry-After and an X-RateLimit-Reason header. Honor it: 10 denials within an hour gets your IP a 24-hour ban (HTTP 403, BANNED). If you're on a shared IP, like an office or CI runner, keep that in mind.

When local FFmpeg or ImageMagick is the better choice

The API runs on the same open-source engines you can install yourself: FFmpeg, ImageMagick, LibreOffice, Ghostscript and Tesseract. That makes the trade-off simple. Use local tools when:

  • Files are sensitive. Contracts, medical scans, anything under NDA. Deletion after 2 hours is still an upload to someone else's server.
  • Volume is high. Thousands of files a day will hit the 1000/day cap. A local ffmpeg loop has no quota.
  • Files are huge. Anything over 200 MB is out of scope.
  • You need exact control. Specific codecs, filter graphs, color profiles or two-pass encodes. The API exposes a curated subset of options, not the full FFmpeg command line.
  • You're offline or latency matters. Uploading a 150 MB video to resize it is slower than doing it on your own machine.

The API makes sense when installing the toolchain is the annoying part: a small VPS without LibreOffice, a serverless function, a browser-only page, a quick script on a borrowed laptop, or a one-off conversion like turning a few iPhone photos into JPGs (the HEIC to JPG converter does the same thing in a browser).

Wrapping up

That's the whole workflow: POST a file with to_format, poll if it's still processing, then download within 2 hours. The full endpoint list, error codes and an OpenAPI spec are in the docs. If something breaks or you need a format that isn't supported, there's a contact form on the site, and I read every message myself.