Everything the site does, from your code: converting, compressing, cutting clips, recognising speech, importing links. JSON over HTTPS, a key in a header, a webhook when done.
The base URL is https://convr.app/api/v1. Requests and responses are UTF-8 JSON; requests with a body need Content-Type: application/json. Times are RFC 3339 in UTC, sizes are bytes, durations are seconds.
The usual flow:
create a job (POST /jobs) with the file's name and size, or with a link;
upload the file straight to storage through the URL you get, then call POST /jobs/{id}/uploaded;
wait for state: "done" — by webhook or by polling GET /jobs/{id};
get a link to the result: GET /jobs/{id}/download.
Files never pass through the API server: uploads and downloads go straight to storage and the CDN, so file size is limited only by your plan.
Quick start
export CONVR_KEY=cvk_…
# 1. create a job: what into what, and the file's name and size
curl -s -X POST https://convr.app/api/v1/jobs -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: application/json" \
-d '{"converter":"mov-mp4","options":{"quality":"medium"},
"file":{"name":"clip.mov","size":52428800}}'
# → {"batch_id":"…","job":{"id":"JOB","state":"awaiting_input",…},
# "upload":{"put_url":"PUT_URL","headers":{"Content-Type":"video/quicktime"}}}
# 2. upload the file straight to storage (no API key here)
curl -X PUT "PUT_URL" -H "Content-Type: video/quicktime" --data-binary @clip.mov
# 3. start it
curl -s -X POST https://convr.app/api/v1/jobs/JOB/uploaded -H "Authorization: Bearer $CONVR_KEY"
# 4. poll until "state":"done", then take the result
curl -s https://convr.app/api/v1/jobs/JOB -H "Authorization: Bearer $CONVR_KEY"
curl -s https://convr.app/api/v1/jobs/JOB/download -H "Authorization: Bearer $CONVR_KEY"
# → {"url":"https://…","expires_at":"…"}
Keys and access
Create a key in your account and send it with every request:
Authorization: Bearer cvk_…
a key acts as your account: the same plan limits, the same file history;
it works on plans with the API only; when the plan ends you get 403 plan_required;
the key is shown once and we keep only its hash; lost it — revoke it and make a new one;
up to 10 keys per account; revoking takes effect at once;
a key cannot manage the account: sign-in, password, payments and keys themselves give 403 not_for_keys.
Never put a key in a web page or a mobile app: it will be pulled out. Call the API from your server.
Limits
Rate: 120 requests a minute per key (with room for short bursts). Beyond that: 429 rate_limited with a Retry-After header in seconds.
Plan: file size, jobs per day, concurrent jobs, video length, speech-recognition minutes and how long results are kept follow your plan. GET /limits returns the exact numbers.
Upload URLs last 15 minutes, result URLs 30 minutes; ask for a fresh result URL any time while the file is kept (the job's expires_at).
Poll no more often than every 2–3 seconds, or use webhooks.
The job object
A job turns one file into one result. Nearly every endpoint returns it.
Stage while running: fetching, probing, converting, uploading.
progress
int
Progress, 0–100.
converter
string
The converter, e.g. mov-mp4.
input_name
string
Name of the source file.
input_size
int
Source size in bytes (for a link, once fetched).
output_name
string
Name of the result.
output_size
int
Result size in bytes (when done).
error
string
Reason code when failed.
expires_at
time
When the files are deleted.
deleted
bool
The files are already deleted.
result
object
Analysis or transcript data (scan and transcribe only).
input_ready
bool
The source is stored; clips and transcribe can use it.
Code
HTTP
Description
awaiting_input
Waiting for the upload.
queued
Queued.
leased
Being processed.
done
✓
Done, ready to download.
failed
✓
Failed, the reason is in error.
canceled
✓
Canceled.
expired
✓
Kept too long, or never uploaded.
Final states are marked ✓ — the job does not change after them.
Converters and options
A converter is named <from>-<to>: mov-mp4, mp4-mp3, png-webp, docx-pdf, pdf-jpg. The full list with each one's options is GET /registry (hundreds of video, audio, image and document pairs). Pick the converter by the source file's extension.
options is a "key: value" object. Everything is optional, omitted keys take their defaults; an unknown key or a value off the list gives 400 invalid_options. Option types in /registry:
Type
Description
enum
string
One of values, e.g. "1080".
int
number
An integer from min to max.
bool
boolean
true or false.
time
number | string
Seconds (12.5) or "mm:ss" / "hh:mm:ss".
text
string
Text up to max characters.
ranges
string
Start-end spans in seconds, comma-separated: "12-27,40.5-70".
asset
string
An image id from POST /assets (watermark).
options
Type
Description
quality
enum
Video: high · medium · small.
target_mb
enum
Video: compress to a size in MB — 8, 10, 16, 25, 50…
Video: recognise speech and burn in subtitles; language; style (tiktok, karaoke, beast, lyrics…).
title / wm_text / wm_image
text / asset
Video: a title, a text or image watermark.
bitrate / sample_rate / channels
enum
Audio: 128–320 kbps; 44100/48000; mono/stereo.
resize / image_quality / strip
enum / int / bool
Images: width; quality 1–100; strip EXIF.
pages / dpi
enum
PDF to images: all or first; 72/150/300.
gif_width / gif_fps
enum
GIF: width and frame rate.
Jobs
POST/jobs
Create a job
Send either file (then upload it through upload) or url — then we fetch the file and the job is queued at once.
The response's upload is one of:
put_url and headers — a file up to 64 MB goes in one PUT with those headers;
part_size, part_count, parts — a larger file goes in parts of part_size bytes (the last one shorter); parts has the first 50 URLs, the rest come from POST /jobs/{id}/parts.
For url the upload block is empty.
Request body (JSON)
Type
Description
converterrequired
string
A converter from /registry, e.g. mov-mp4.
options
object
Converter options.
file.name
string
File name (up to 1024 chars). With file.size — instead of url.
file.size
int
The exact file size in bytes.
url
string
A link instead of a file: direct, Google Drive, Dropbox, YouTube, TikTok, Instagram, Vimeo, X.
webhook
string
Where to POST when the job finishes.
save_to_drive
bool
Put the result into the account’s cloud when it is ready.
drive_folder
string
The cloud folder for the result (the top level by default).
batch_id
string
Add the job to an existing batch (for ZIP and events).
unknown_converter400 No such converter — see GET /registry.
invalid_options400 Unknown option or a value not allowed.
invalid_file400 The file name or size is missing.
invalid_url400 The link is not http(s), or a file was sent with it.
invalid_webhook400 The webhook address is not http(s).
file_too_large413 The file is larger than the plan allows (limit in bytes).
limit_active429 Too many jobs running at once.
limit_daily429 The daily job limit is used up.
speech_limit429 Speech-recognition minutes for 30 days are used up.
links_unavailable503 Downloading from video sites is unavailable for now.
PUT{put_url}
Upload the file
This request goes straight to storage — without the API key, with the headers from upload.headers. The body is the file itself, exactly file.size bytes. The URL lasts 15 minutes.
For parts: PUT each part to its url with part_size bytes (the last one is the rest). Parts may go in parallel and in any order; you do not assemble them.
Example
curl -X PUT "$PUT_URL" -H "Content-Type: video/mp4" --data-binary @video.mp4
# parts: split -b 16777216 big.mov part_ && upload part_aa to parts[0].url, …
curl -X PUT "$PART_1_URL" --data-binary @part_aa
POST/jobs/{id}/parts
More part URLs
Signed URLs for parts from through to, at most 50 at a time.
Every job belongs to a batch (batch_id in the create response). Pass the same batch_id to later POST /jobs to follow them with one event stream or pack them into a ZIP.
Server-Sent Events: a snapshot with all jobs first, then a job event on every change. The connection stays open; reconnect after a drop and a new snapshot comes.
Makes an archive job of all finished results in the batch (two or more). Calling again for the same set returns the same archive.
Response
{ "id": "…", "state": "…", … } // the job object
Errors
nothing_to_zip400 Fewer than two finished results in the batch.
zip_too_large413 The archive would be too large.
Cloud
The account’s files with no expiry, in folders, within the plan’s space. Uploads work as for jobs: a PUT link or parts. A cloud file converts with no new upload; the result lands in the same folder.
GET/drive
Folder contents
Folders and files, the way to the folder, space used and the quota.
file_too_large413 The file is larger than the plan allows (limit in bytes).
POST/drive/files/{id}/convert
Convert from the cloud
Creates a job that reads the file right from the cloud; the result goes into the file’s folder (save: false to skip).
Request body (JSON)
Type
Description
converterrequired
string
A converter for the file’s format.
options
object
Options.
folder
string
Another folder for the result.
Response
{ "id": "…", "state": "…", … } // the job object
POST/jobs/{id}/drive
A result into the cloud
Copies a finished job’s result into the cloud (inside storage, nothing downloaded).
Request body (JSON)
Type
Description
folder
string
The folder.
Errors
drive_full
not_ready409 No result yet: the job is not done.
GET/drive/files/{id}/url
Download link
A signed CDN link for a while.
Response
{ "url": "https://…", "expires_at": "…" }
PATCH/drive/files/{id}
Rename or move
name and/or folder ("" for the top level). Folders: PATCH /drive/folders/{id} with name and parent.
DELETE/drive/files/{id}
Delete a file
Frees the space at once. A folder goes with its contents: DELETE /drive/folders/{id}.
Clips and speech
A video is uploaded once as an analysis job <format>-scan; then you can cut clips and recognise speech from it as often as you like, with no new upload, while the job is kept.
POST/jobs {"converter":"mp4-scan"}
Analyse a video
A regular job with the mp4-scan converter (mov-scan, mkv-scan…). The analysis is in the finished job’s result: loudness, scene changes and the best moments for 15, 30, 60 and 90-second clips scored 0–100. Analysing an uploaded file does not count toward the daily job limit.
{id} is the scan job. One span gives an MP4, several give a ZIP of clips. Video options apply: aspect (9:16 by default), fit, quality, title, watermark, filters, music.
Request body (JSON)
Type
Description
options.rangesrequired
ranges
Up to 20 spans, 1–600 s each.
options.*
Other options of the <format>-clips converter in /registry.
not_uploaded409 The scan job’s video is not uploaded or fetched yet.
source_gone409 The scan job was canceled or expired — the file is gone.
invalid_options400 Unknown option or a value not allowed.
limit_active429 Too many jobs running at once.
limit_daily429 The daily job limit is used up.
POST/jobs/{id}/transcribe
Recognise speech
{id} is the scan job. Words with timings come in the finished job’s result. Up to 20 minutes of speech per job; it uses the plan’s recognition minutes.
Request body (JSON)
Type
Description
options.rangesrequired
ranges
Which spans to recognise.
options.lang
enum
auto, ru, uk, en, de, es, fr, it, pl, pt, tr, kk, be.
job.done carries download_url, a result link for 30 minutes; later, get a new one from /download;
answer with any 2xx within 10 seconds. Otherwise we retry after 10 s, 40 s, ~3 min, ~11 min and ~43 min — 6 attempts in all;
the address must be public: private networks and redirects are not supported;
an event may arrive twice (say, a network failure after you answered) — check job.id.
Every call is signed: X-Convr-Signature: sha256=<hex> is an HMAC-SHA256 of the raw request body with the whsec_… secret from your account. Check it before parsing the JSON and compare in constant time.
import hashlib, hmac, os
from flask import Flask, abort, request
SECRET = os.environ["CONVR_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/hooks/convr")
def convr_hook():
sig = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, request.headers.get("X-Convr-Signature", "")):
abort(401)
e = request.get_json()
if e["event"] == "job.done":
print(e["job"]["id"], e["download_url"])
return "", 204
Errors
A failed request returns an HTTP 4xx/5xx status and a body of {"error": "code"}. Limit errors also carry limit, the number you hit. Rely on the code, not on text: codes do not change.
HTTP/1.1 429 Too Many Requests
{"error": "limit_active", "limit": 10}
Code
HTTP
Description
invalid_key
401
No such key, it was revoked, or the account is disabled.
plan_required
403
The account’s plan has no API.
not_for_keys
403
This endpoint is for the website only.
rate_limited
429
Over 120 requests a minute. Wait Retry-After seconds.
limit_active
429
Too many jobs running at once.
limit_daily
429
The daily job limit is used up.
speech_limit
429
Speech-recognition minutes for 30 days are used up.
file_too_large
413
The file is larger than the plan allows (limit in bytes).
json_required
415
Missing Content-Type: application/json.
bad_json
400
The body is not JSON or fields have wrong types.
unknown_converter
400
No such converter — see GET /registry.
invalid_options
400
Unknown option or a value not allowed.
invalid_file
400
The file name or size is missing.
invalid_url
400
The link is not http(s), or a file was sent with it.
invalid_webhook
400
The webhook address is not http(s).
not_found
404
No such job, or it is not yours.
already_uploaded
409
The job has already started.
upload_incomplete
400
The file (or a part) is not fully uploaded.
upload_size_mismatch
400
The uploaded size differs from file.size.
not_ready
409
No result yet: the job is not done.
links_unavailable
503
Downloading from video sites is unavailable for now.
speech_unavailable
503
Speech recognition is unavailable for now.
internal
500
Our failure. Try again later.
When a job ends with state: "failed", the reason is in its error field:
Code
Description
input_invalid
The file is damaged or unreadable.
input_format_mismatch
The file is not in the converter’s format.
input_no_stream
The needed stream is missing (e.g. audio for mp4-mp3).
input_encrypted
The archive is password-protected.
input_too_long
The video is longer than the plan allows.
input_too_large
The linked file is over the plan’s limit.
input_resolution
The resolution is too high.
target_too_small
The video cannot fit target_mb even at the lowest quality.
output_too_large
The result came out over the limit.
convert_failed
Conversion failed (after retries on other servers).
timeout
Processing took too long.
url_unreachable
The link does not open.
url_blocked
The link points to a private network — not allowed.
no_video
There is no video at the link (a photo post).
url_private
The video is private, removed, or age- or region-restricted.
site_blocked
The site refuses for now. Try again later.
fetch_failed
The uploaded file could not be read.
speech_too_long
Over 20 minutes of speech in one job.
worker_lost
The processing server vanished; the job is usually retried.