convr Sign in

REST · JSON · Webhooks

API for developers

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.

Get a key → Base URLhttps://convr.app/api/v1

Overview

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.

{
  "id": "01m6he8nmn6ykgpp3mzh4mek06",
  "state": "done",
  "progress": 100,
  "converter": "mov-mp4",
  "input_name": "clip.mov",
  "input_size": 52428800,
  "output_name": "clip.mp4",
  "output_size": 18874368,
  "expires_at": "2026-11-05T10:00:00Z",
  "input_ready": true
}
TypeDescription
idstringJob id, 26 characters.
statestringState — see the table below.
stagestringStage while running: fetching, probing, converting, uploading.
progressintProgress, 0–100.
converterstringThe converter, e.g. mov-mp4.
input_namestringName of the source file.
input_sizeintSource size in bytes (for a link, once fetched).
output_namestringName of the result.
output_sizeintResult size in bytes (when done).
errorstringReason code when failed.
expires_attimeWhen the files are deleted.
deletedboolThe files are already deleted.
resultobjectAnalysis or transcript data (scan and transcribe only).
input_readyboolThe source is stored; clips and transcribe can use it.
CodeHTTPDescription
awaiting_inputWaiting for the upload.
queuedQueued.
leasedBeing 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:

TypeDescription
enumstringOne of values, e.g. "1080".
intnumberAn integer from min to max.
boolbooleantrue or false.
timenumber | stringSeconds (12.5) or "mm:ss" / "hh:mm:ss".
textstringText up to max characters.
rangesstringStart-end spans in seconds, comma-separated: "12-27,40.5-70".
assetstringAn image id from POST /assets (watermark).
optionsTypeDescription
qualityenumVideo: high · medium · small.
target_mbenumVideo: compress to a size in MB — 8, 10, 16, 25, 50…
resolutionenumVideo: frame height — keep, 2160 … 360.
aspect / fitenumVideo: 9:16, 4:5, 1:1, 16:9; blur bars, crop, or smart (follows faces).
trim_start / trim_endtimeVideo and audio: trim the start and end.
vcodec / fpsenumVideo: h264 or h265; frame rate.
no_audio / volumebool / intDrop the audio; volume 0–300 %.
auto_subs / sub_lang / sub_stylebool / enumVideo: recognise speech and burn in subtitles; language; style (tiktok, karaoke, beast, lyrics…).
title / wm_text / wm_imagetext / assetVideo: a title, a text or image watermark.
bitrate / sample_rate / channelsenumAudio: 128–320 kbps; 44100/48000; mono/stereo.
resize / image_quality / stripenum / int / boolImages: width; quality 1–100; strip EXIF.
pages / dpienumPDF to images: all or first; 72/150/300.
gif_width / gif_fpsenumGIF: 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)

TypeDescription
converterrequiredstringA converter from /registry, e.g. mov-mp4.
optionsobjectConverter options.
file.namestringFile name (up to 1024 chars). With file.size — instead of url.
file.sizeintThe exact file size in bytes.
urlstringA link instead of a file: direct, Google Drive, Dropbox, YouTube, TikTok, Instagram, Vimeo, X.
webhookstringWhere to POST when the job finishes.
save_to_driveboolPut the result into the account’s cloud when it is ready.
drive_folderstringThe cloud folder for the result (the top level by default).
batch_idstringAdd the job to an existing batch (for ZIP and events).

Example

curl -s -X POST https://convr.app/api/v1/jobs -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: application/json" \
  -d '{"converter":"mp4-mp4","options":{"target_mb":"25"},
       "file":{"name":"video.mp4","size":187654321}}'

Response

201 Created
{
  "batch_id": "01m6he8m2e…",
  "job": { "id": "01m6he8nmn…", "state": "awaiting_input", … },
  "upload": {
    "put_url": "https://…",
    "headers": { "Content-Type": "video/quicktime" }
  }
}

// a file over 64 MB:
  "upload": {
    "part_size": 16777216,
    "part_count": 32,
    "parts": [{ "n": 1, "url": "https://…" }, …]
  }

Errors

  • 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.

Request body (JSON)

TypeDescription
fromrequiredintFirst part number (from 1).
torequiredintLast part number.

Example

curl -s -X POST https://convr.app/api/v1/jobs/JOB/parts -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: application/json" -d '{"from":51,"to":100}'

Response

{ "parts": [{ "n": 51, "url": "https://…" }, …] }

Errors

  • bad_range400 from/to outside the file’s parts, or over 50 at once.
  • no_upload409 The job has no open multipart upload.
POST /jobs/{id}/uploaded

Start after upload

Checks the file is fully uploaded and of the declared size, and queues the job. No body.

Example

curl -s -X POST https://convr.app/api/v1/jobs/JOB/uploaded -H "Authorization: Bearer $CONVR_KEY"

Response

{ "id": "…", "state": "…", … }   // the job object

Errors

  • upload_incomplete400 The file (or a part) is not fully uploaded.
  • upload_size_mismatch400 The uploaded size differs from file.size.
  • already_uploaded409 The job has already started.
GET /jobs/{id}

Job status

Current state, stage and progress.

Example

curl -s https://convr.app/api/v1/jobs/JOB -H "Authorization: Bearer $CONVR_KEY"

Response

{ "id": "…", "state": "leased", "stage": "converting", "progress": 42, … }

Errors

  • not_found404 No such job, or it is not yours.
GET /jobs/{id}/download

Result URL

A signed CDN URL for 30 minutes (never past the job’s expiry). Download it without the key. Ask again whenever needed.

Example

curl -sL "$(curl -s https://convr.app/api/v1/jobs/JOB/download -H "Authorization: Bearer $CONVR_KEY" | jq -r .url)" -o result.mp4

Response

{ "url": "https://dl.…/clip.mp4?token=…&expires=…", "expires_at": "2026-11-04T12:30:00Z" }

Errors

  • not_ready409 No result yet: the job is not done.
  • not_found404 No such job, or it is not yours.
POST /jobs/{id}/cancel

Cancel

Stops the job if it has not finished.

Example

curl -s -X POST https://convr.app/api/v1/jobs/JOB/cancel -H "Authorization: Bearer $CONVR_KEY"

Response

{ "id": "…", "state": "…", … }   // the job object
DELETE /jobs/{id}

Delete the files

Cancels the job and deletes the source and result now instead of at expiry.

Example

curl -s -X DELETE https://convr.app/api/v1/jobs/JOB -H "Authorization: Bearer $CONVR_KEY"

Response

204 No Content

Batches

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.

GET /batches/{id}

Batch jobs

All jobs of the batch in creation order.

Response

{ "id": "…", "jobs": [ { "id": "…", "state": "done", … }, … ] }
GET /batches/{id}/events

Event stream (SSE)

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.

Example

curl -N https://convr.app/api/v1/batches/BATCH/events -H "Authorization: Bearer $CONVR_KEY"

Response

event: snapshot
data: [{"id":"…","state":"queued",…}]

event: job
data: {"id":"…","state":"leased","stage":"converting","progress":37,…}
POST /batches/{id}/zip

Pack into a ZIP

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.

Query parameters

TypeDescription
folderstringThe folder; the top level without it.
qstringSearch by name in every folder.

Response

{ "folder": null, "path": [], "folders": [ { "id": "…", "name": "Работа", "files": 3, "size": 1048576 } ],
  "files": [ { "id": "…", "name": "report.pdf", "size": 52011, "format": "pdf", "state": "ready" } ],
  "used": 1100587, "quota": 2147483648, "count": 4 }
POST /drive/files

Upload a file

Reserves the space and returns where to upload (as POST /jobs). After the upload — POST /drive/files/{id}/uploaded.

Request body (JSON)

TypeDescription
namerequiredstringFile name.
sizerequiredintSize in bytes.
folderstringThe folder.

Response

{ "file": { "id": "…", "state": "uploading", … }, "upload": { "put_url": "…", "headers": { … } } }

Errors

  • drive_full
  • drive_unavailable
  • 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)

TypeDescription
converterrequiredstringA converter for the file’s format.
optionsobjectOptions.
folderstringAnother 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)

TypeDescription
folderstringThe 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.

Response

"result": {
  "duration": 312.4,
  "loudness": [0.12, 0.4, …],
  "scenes": [4.2, 17.9, …],
  "clips": {
    "30": [{ "start": 81.5, "end": 111.5, "score": 100 }, …],
    "60": [ … ]
  }
}
POST /jobs/{id}/clips

Cut clips

{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)

TypeDescription
options.rangesrequiredrangesUp to 20 spans, 1–600 s each.
options.*Other options of the <format>-clips converter in /registry.
webhookstringAs in POST /jobs.

Example

curl -s -X POST https://convr.app/api/v1/jobs/SCAN/clips -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: application/json" \
  -d '{"options":{"ranges":"81.5-111.5,140-170","aspect":"9:16","fit":"smart",
       "title":"Best moments"}}'

Response

201 Created
{ "id": "…", "converter": "mp4-clips", "state": "queued", … }

Errors

  • not_a_scan400 {id} is not an analysis (scan) job.
  • 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)

TypeDescription
options.rangesrequiredrangesWhich spans to recognise.
options.langenumauto, ru, uk, en, de, es, fr, it, pl, pt, tr, kk, be.

Example

curl -s -X POST https://convr.app/api/v1/jobs/SCAN/transcribe -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: application/json" -d '{"options":{"ranges":"0-60","lang":"auto"}}'

Response

"result": {
  "language": "ru",
  "clips": [{
    "start": 0, "end": 60,
    "words": [{ "w": "Привет", "s": 0.32, "e": 0.71 }, …]
  }]
}

Errors

  • speech_too_long400 Over 20 minutes of speech (limit in seconds).
  • speech_limit429 Speech-recognition minutes for 30 days are used up.
  • speech_unavailable503 Speech recognition is unavailable for now.
  • not_a_scan400 {id} is not an analysis (scan) job.

Reference and account

GET /registry

Formats and converters

All formats, converters and their options. No key needed. Changes rarely — cache it by version.

Response

{
  "version": 14,
  "formats": [{"id": "mov", "ext": "mov", "kind": "video", "mime": "video/quicktime"}, …],
  "converters": [{
    "id": "mov-mp4", "from": "mov", "to": "mp4", "engine": "ffmpeg",
    "options": [
      {"key": "quality", "type": "enum", "values": ["high", "medium", "small"], "default": "medium"},
      {"key": "trim_start", "type": "time", "max": 86400},
      …
    ]
  }, …],
  "tools": [ … ]
}
GET /limits

Plan limits

Your plan’s limits and the recognition seconds used.

Response

{
  "max_file_bytes": 10737418240,
  "max_jobs_day": 500,
  "max_active": 10,
  "max_duration_s": 14400,
  "retention_h": 72,
  "speech_minutes": 600,
  "speech_used_s": 1834.5
}
GET /account/jobs

History

The account’s jobs, newest first, 50 at a time. For the next page pass the last created_at as before.

Query parameters

TypeDescription
beforetimeCursor: RFC 3339.

Response

{ "jobs": [{ "id": "…", "state": "done", …, "created_at": "2026-11-04T09:12:44.123Z" }, …] }

Errors

  • bad_cursor400 before is not an RFC 3339 time.
POST /assets

Watermark image

The body is a PNG up to 2 MB and 2048×2048 (Content-Type: image/png). Pass the id you get as options.wm_image.

Example

curl -s -X POST https://convr.app/api/v1/assets -H "Authorization: Bearer $CONVR_KEY" -H "Content-Type: image/png" --data-binary @logo.png

Response

201 Created
{ "id": "…", "width": 512, "height": 512 }

Errors

  • invalid_image400 Not a PNG, or over 2048×2048.
  • file_too_large413 The file is larger than the plan allows (limit in bytes).
  • too_many_assets429 Too many images in a day.

Webhooks

Pass webhook when creating a job (or clips). When the job reaches a final state, we POST JSON to that address.

  • events: job.done, job.failed, job.canceled, job.expired;
  • 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.

POST /hooks/convr HTTP/1.1
Content-Type: application/json
User-Agent: convr-webhook
X-Convr-Event: job.done
X-Convr-Job: 01m6he8nmn6ykgpp3mzh4mek06
X-Convr-Signature: sha256=5d41402abc4b2a76b9719d911017c592…

{
  "event": "job.done",
  "job": { "id": "01m6he8nmn6ykgpp3mzh4mek06", "state": "done", "output_name": "clip.mp4", … },
  "download_url": "https://dl.…/clip.mp4?token=…",
  "sent_at": "2026-11-04T10:15:02Z"
}
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}
CodeHTTPDescription
invalid_key401No such key, it was revoked, or the account is disabled.
plan_required403The account’s plan has no API.
not_for_keys403This endpoint is for the website only.
rate_limited429Over 120 requests a minute. Wait Retry-After seconds.
limit_active429Too many jobs running at once.
limit_daily429The daily job limit is used up.
speech_limit429Speech-recognition minutes for 30 days are used up.
file_too_large413The file is larger than the plan allows (limit in bytes).
json_required415Missing Content-Type: application/json.
bad_json400The body is not JSON or fields have wrong types.
unknown_converter400No such converter — see GET /registry.
invalid_options400Unknown option or a value not allowed.
invalid_file400The file name or size is missing.
invalid_url400The link is not http(s), or a file was sent with it.
invalid_webhook400The webhook address is not http(s).
not_found404No such job, or it is not yours.
already_uploaded409The job has already started.
upload_incomplete400The file (or a part) is not fully uploaded.
upload_size_mismatch400The uploaded size differs from file.size.
not_ready409No result yet: the job is not done.
links_unavailable503Downloading from video sites is unavailable for now.
speech_unavailable503Speech recognition is unavailable for now.
internal500Our failure. Try again later.

When a job ends with state: "failed", the reason is in its error field:

CodeDescription
input_invalidThe file is damaged or unreadable.
input_format_mismatchThe file is not in the converter’s format.
input_no_streamThe needed stream is missing (e.g. audio for mp4-mp3).
input_encryptedThe archive is password-protected.
input_too_longThe video is longer than the plan allows.
input_too_largeThe linked file is over the plan’s limit.
input_resolutionThe resolution is too high.
target_too_smallThe video cannot fit target_mb even at the lowest quality.
output_too_largeThe result came out over the limit.
convert_failedConversion failed (after retries on other servers).
timeoutProcessing took too long.
url_unreachableThe link does not open.
url_blockedThe link points to a private network — not allowed.
no_videoThere is no video at the link (a photo post).
url_privateThe video is private, removed, or age- or region-restricted.
site_blockedThe site refuses for now. Try again later.
fetch_failedThe uploaded file could not be read.
speech_too_longOver 20 minutes of speech in one job.
worker_lostThe processing server vanished; the job is usually retried.
internalInternal error.