3. Operations catalog
Every operation is addressed by a service_slug + operation_key pair and
takes its input as params.file_url.
The live, always-current list is one public request (no key needed) — this is the same data the panel's catalog renders from:
curl -s https://metaplatho.com/api/v1/studio/tools
Each entry gives you operation_id, service_slug, operation_key, its rate
card tariff_class and that card's base credits_per_call, what file kinds it
accepts, its tunable parameters
(config.fields[], each with key / type / min / max), and its ready-made
settings (presets[], each {index, label, params}), the rest of its params
(config.api_params[], each {key, default, values}) and example — a
{service_slug, operation_key, params} that works as-is.
credits_per_call is what one job costs at submit — the BASE of the
operation's rate card (tariff_class), the floor of the job's cost and not its
total. A params.operations pipeline is priced at the dearest class of any
step in it: addressing a cheap operation while shipping an expensive recipe
costs exactly what addressing the expensive one does.
The tariff: you pay for processing time, at one of four rates
Every operation is metered by the worker's clock. The rate depends on the
operation's class (the shipped defaults; the live numbers are in
GET /api/v1/public/config → billing.tariff_classes):
| class | what is in it | base / file | + per started minute | never more than |
|---|---|---|---|---|
photo |
every image operation | 1 | 1 | 5 |
light |
video with no re-encode: remux, keyframe trim, one frame, audio-only | 1 | 1 | 10 |
standard |
video re-encodes: compress, convert, resize, social, effects | 2 | 2 | 40 |
speech |
subtitles, transcribe, auto-edit — speech recognition on a GPU | 5 | 3 | 60 |
So a standard job: up to 1 min of processing costs 2, 1–2 min costs 4, 2–3 min
costs 6, and never more than 40 for one file. A started minute is a whole
minute: 61 seconds is two minutes. Processing time is measured on the worker
from the moment it picks your file up — the queue wait is not billed.
Ask before you submit. POST /api/v1/studio/quote (public, no auth, moves
no money) prices real files on real tools from what you already know about
them:
curl -s -X POST https://metaplatho.com/api/v1/studio/quote -H 'Content-Type: application/json' -d '{
"files": [{"kind": "video", "duration_sec": 300, "width": 1920, "height": 1080, "size_bytes": 52000000}],
"operation_ids": [44, 51]
}'
Each tool in the answer carries its tariff_class, the card's three numbers,
and per file a likely cost, a max (the cap) and an est_processing_sec;
known: false means the length was guessed from the bytes. The studio shows
exactly this on every tile the moment a file is dropped.
Nobody knows how long your file will take until a worker has run it, so the flow is:
- at submit you are charged the card's base, and
estimated_costreflects that; the job'stariff_classsays which card; - when the job finishes, the difference is debited and
final_costbecomes the real total. Faster than a minute changes nothing — the base is the minimum and is never refunded down.
A job that ends failed or canceled is never charged for time. If it
ended within its first 60 seconds of worker time, everything is refunded and
final_cost is 0; after that the base is kept (final_cost = base) — the
worker ran the file and what it hit was the input. An input over your tier's
length limit is always refunded in full. If we cannot measure the time at all,
you pay the base and nothing more.
Don't hardcode these numbers — GET /api/v1/public/config returns the four
cards under billing.tariff_classes (base_credits, per_minute_credits,
max_credits, plus the estimate's work_pct and fixed_sec) and the switch
time_tariff_enabled. The historical time_tariff_* fields are the standard
card under their old names.
The input ceilings: length and size
The per-file cap bounds your bill; it does not bound our workers. So an input
longer than your tier allows is refused, not truncated: the job ends
failed, every bonus it took is refunded, and error names the length, the
limit and your tier. A file bigger than your tier allows never gets that far:
it is refused with 413 when the upload is opened (/uploads/multipart/create,
/uploads/multipart/complete, upload links), before a byte is stored.
| tier | max input length, any operation | max size per file |
|---|---|---|
| anonymous (no sign-in) | 5 min | 300 MB |
| free account | 5 min | 1 GB |
| paid plans | 45 min | 6 GB |
The length ceiling applies to every operation, not just speech-to-text, and
only to media that reports a duration (images are never affected); the size cap
applies to every file. Live values are on GET /api/v1/public/config under
billing.input_max_minutes and billing.input_max_mb (billing.video_max_mb /
video_max_sec carry the largest tier's pair for older clients).
Split a longer recording yourself (trim is a remux — it is cheap and lossless)
and submit the parts.
Bulk works the same way, per file. A /studio/batch is charged the base per
file at submit and each file's own processing time afterwards, and the length
ceiling applies per file too. A file longer than your tier allows stops the
batch: the remaining files are canceled, the batch ends failed with the
filename in error, and everything not already delivered is refunded, the
over-long file included. Files delivered before it stay charged.
Some jobs — compress, resize, convert, watermark, add/strip metadata — exist on
both services, once for video and once for images. Those entries share a
config.family value. The web studio uses it to show a single button and picks
the operation from the file you dropped; over the API you address the two
operations separately, as usual, by service_slug + operation_key. The tables below are the
shipped baseline — an admin can add or retune operations, so treat
/studio/tools as the source of truth.
ClipConvert — video ("service_slug": "clipconvert")
Every operation accepts video; transcribe also takes audio-only files.
Class is the rate card the job is priced at — photo 1 (+1/min, cap 5),
light 1 (+1/min, cap 10), standard 2 (+2/min, cap 40), speech 5 (+3/min, cap
60) at the time of writing. The base debited at
submit is the card's base_credits; the live number per operation is
credits_per_call in GET /api/v1/studio/tools, the cards themselves in
GET /api/v1/public/config → billing.tariff_classes. Two operations change
class with their params: subtitles with your own text, cues or a file is
standard (light with mode: "embed"), and auto_edit with cuts is
standard — no speech is recognised in either case.
operation_key |
What it does | Class | Main params |
|---|---|---|---|
compress |
Shrink for web | standard | level light|medium|strong (medium), crf 0–51, preset ultrafast…veryslow (veryfast), max_mb 0–2000 (fit into this size — see below) |
resize |
Change resolution | standard | width / height (or long_side), mode fit|pad, bg_color for the letterbox (#000000) |
convert |
Change container | standard | format mp4|webm|mov|mkv; vcodec libx264|libx265|libvpx-vp9|copy, acodec aac|libmp3lame|libopus|copy (omit → the container's default) |
social |
Social presets | standard | preset square|vertical|widescreen (required), mode crop|pad, focus_x/focus_y 0–1 (where the crop sits: 0 = left/top edge, 0.5 centre) |
trim |
Cut a segment | light | start (s), duration (s), precise (default true) |
fps |
Change frame rate | standard | fps 1–120 |
faststart |
Web-optimize (instant play) | light | — |
mute |
Remove the audio track | light | — |
speed |
Speed up / slow down | standard | factor >0.1–10 |
rotate |
Rotate | standard | mode 90cw|90ccw|180 |
flip |
Mirror | standard | axis h|v |
gif |
Video → GIF | standard | start, duration ≤60, fps 1–30 (15), width 16–1920 (600) |
thumbnail |
Grab a frame | light | at (s), smart, format jpg|png|webp (jpeg is accepted as an alias for jpg), width 16–3840 |
cover |
Cover image | light | the studio's cover picker; over the API call thumbnail with {"smart": true} (and at) — it is listed in the catalog but has no params of its own |
extract_audio |
Rip the audio | light | format mp3|aac |
enhance_voice |
Clean up voice | light | level light|medium|strong; denoise, deess, compress, loudnorm |
add_audio |
Voiceover / background music | light | tracks[], original_volume — see Add audio |
subtitles |
Burn / embed subtitles | speech | see Subtitles |
transcribe |
Speech → SRT/VTT/TXT | speech | format, language, language_hint, task, quality fast|balanced|accurate, typography |
auto_edit |
Cut pauses & filler words | speech | language, max_pause 0.2–5, keep_pause 0–2, padding 0–1 (0.12), remove_fillers, remove_retakes, filler_words[] ≤50 |
circle |
Round video / Telegram note | standard | see Circle |
blur_bg |
Fit to canvas, blurred bg | standard | see Fit to canvas |
watermark |
Overlay a watermark | standard | text (≤120) or logo_url; position tl|tc|tr|ml|mc|mr|bl|bc|br|center (default br), opacity 0.05–1 (0.5), size 0–100 % of width (6), margin 0–40 (2), color (#ffffff) |
device_meta |
Tag as shot on a device | light | preset (device key), creation_time — writes container/QuickTime tags, not EXIF |
strip_metadata |
Remove metadata | light | — |
Fit into a size: compress + max_mb. The output comes out no bigger
than max_mb megabytes (1 MB = 1 048 576 bytes) in ONE pass — the video
bitrate is capped from the duration ((max_mb·8192·0.95 / seconds) − audio),
the quality setting still applies below the cap, and if the cap falls under
~1.5 Mbit/s the video steps down to 720p. {"max_mb": 49} fits a Telegram
bot's 50 MB upload. Too small a target for the length fails before any work
with max_mb 1 is too small for a 600-second video — at least 19.0 MB.
result.body.size_target reports {max_mb, size_mb, met, video_kbps_cap, audio_kbps, downscaled_to_720p}.
Which video operations re-encode (and which don't)
An operation that changes no pixels is a remux: the picture is copied packet for packet instead of being re-encoded. It comes back in seconds instead of minutes, and it is bit-identical — no quality loss, no bitrate change.
Frame rate on re-encodes. A re-encode keeps the source frame rate up to
60 fps; faster footage (120/240 fps slow-motion from a phone) comes out at
60 fps — the result reports fps_capped_from. The Business plan has no cap.
An explicit fps operation always wins, and a remux keeps the original rate.
Input resolution. Video up to 4K (3840×2160, either orientation) on every
plan; Pro and Business take up to 8K (8192×4320 — GoPro 5.3K, DJI,
8K phones, 5K screen recordings). Larger input fails with video too large.
Re-encodes of big inputs come out web-sized either way.
| Behaviour | Operations |
|---|---|
| Remux — nothing is re-encoded | faststart, mute, strip_metadata, device_meta, trim from 0 (or with precise: false), convert when the codecs already fit the new container (e.g. .mov H.264 → .mp4), subtitles with mode: "embed", extract_audio when the source track already is that format (AAC → aac) |
| Sound only — the picture is copied | enhance_voice, add_audio |
| Full re-encode — required | compress, resize, social, fps, rotate, flip, speed, watermark, blur_bg, circle, gif, subtitles with mode: "burn", auto_edit, convert to a different codec (anything → webm) |
Two consequences worth knowing:
trimaccuracy.precise: true(the default) re-encodes so the cut lands exactly onstart.precise: falsecuts on the nearest keyframe at or beforestartand copies — near-instant, but the clip can begin up to a few seconds early. A trim from0always takes the fast path; the flag only matters for a cut from mid-video.- Resolution. A re-encode is capped at 1080p. A remux is not — the video is
passed through exactly as you sent it, 4K included. Ask for a smaller file
explicitly with
resizeorcompress.
The finished job reports which path actually ran at result.body.encode_mode —
one of remux, video_copy, audio_copy, encode.
PhotoConvert — images ("service_slug": "photoconvert")
Every operation accepts image.
operation_key |
What it does | Class | Main params |
|---|---|---|---|
resize |
Change dimensions | photo | width / height 16–20000 or long_side, resize_mode fit|fill|exact|pad|long, upscale, bg_color (pad); scale by percentage needs both percent and resize_mode: "scale" |
compress |
Shrink | photo | quality 1–100, format jpeg|webp|avif|png (also tiff, bmp, gif, heic), lossless, max_kb (re-encodes down to fit) |
convert |
Change format | photo | format webp|avif|jpeg|png, quality, max_kb |
crop |
Crop to aspect | photo | box [left, top, right, bottom] px, or crop_aspect 1:1|4:5|5:4|4:3|3:4|3:2|2:3|16:9|9:16|21:9, gravity center|top|bottom|left|right |
circle |
Round avatar | photo | diameter 16–8000, zoom, offset_x/y, bg_color (omit → transparent PNG) |
transform |
Rotate / flip | photo | rotate 0–359 counter-clockwise (90/180/270 lossless, other angles fill the corners with bg_color), flip h|v |
enhance |
Filters | photo | autocontrast, brightness / contrast / saturation / sharpness 0–4 (1 = unchanged), blur 0–100 px, grayscale |
social |
Social sizes | photo | any width/height 16–20000, resize_mode (presets use fill) |
web_optimize |
Optimize for web | photo | format, quality, max_kb; add "strip_metadata": true (and "strip_gps": true) to drop EXIF/GPS — not implicit |
strip_meta |
Remove EXIF / GPS | photo | strip_metadata, strip_gps; GPS only → {"metadata_mode": "merge", "strip_gps": true} |
device_meta |
Write camera EXIF | photo | preset (see below), metadata_mode: "set", fields (extra tags), gps |
watermark |
Overlay a watermark | photo | text or logo_url, position (9-grid tl…br), opacity 0.05–1, size (% of width), margin 0–40, color |
edit |
Everything in one pass | photo | composes a full pipeline |
device_meta presets: iphone, iphone_15, iphone_15_pro_max, iphone_16,
iphone_16_pro, iphone_16_pro_max, samsung_galaxy_s24,
samsung_galaxy_s24_ultra, samsung_galaxy_s25_ultra, google_pixel_8_pro,
google_pixel_9_pro, xiaomi_14_ultra, oneplus_12, sony_xperia_1_vi,
huawei_p60_pro, sony_a7_iv, canon_eos_r5, canon_eos_r6_mark_ii,
nikon_z8, fujifilm_x_t5.
Send the params. An image operation whose params say nothing to do (
watermarkwith no text and no logo,editwith no pipeline) is rejected with an error — it is never accepted, billed, and answered with your own file back.
There is no separate audio service. Audio work is done by the ClipConvert operations:
transcribetakes an audio-only file directly (MP3, WAV, M4A, OGG/Opus, FLAC — itsacceptsis["video", "audio"]);extract_audio,muteandenhance_voiceread the audio track out of a video and refuse a bare.mp3/.wavwith400 this operation works on video files — the file you sent looks like audio.
DocConvert — PDF ("service_slug": "docconvert")
PDF in and out. merge_pdfs and images_to_pdf take several files as one
job — send them in order as repeated files fields (or params.file_urls on
/jobs/api); everything else takes one PDF. Every result is a download
(Content-Disposition: attachment).
operation_key |
What it does | Class | Main params |
|---|---|---|---|
images_to_pdf |
Photos / scans → one PDF, in the order sent | photo | page_size fit|a4|letter (fit = each page the photo's size), orientation auto|portrait|landscape, margin_mm 0–50 |
merge_pdfs |
Several PDFs → one, in the order sent | photo | — (2 or more files) |
compress_pdf |
Smaller PDF — pictures inside recompressed, text untouched | photo | level low|medium|high (default medium) |
pdf_to_images |
Pages → JPG / PNG / WebP | photo | format jpg|png|webp, dpi 50–300 (150), quality 1–100 (85), pages |
pdf_to_text |
The PDF's text as a UTF-8 .txt |
photo | pages |
pages picks pages, 1-based: "1-3,5", "2-" (to the end), "last"; empty =
all. One page from pdf_to_images comes back as a picture, several as a ZIP of
page-001.jpg, … (result.outputs[0].contains lists them).
Several photos → one PDF (the Telegram "album → PDF" case):
curl -s https://metaplatho.com/api/v1/jobs/upload -H 'X-Api-Key: ca_live_...' \
-F '[email protected]' -F '[email protected]' -F '[email protected]' \
-F 'service_slug=docconvert' -F 'operation_key=images_to_pdf' \
-F 'params={"page_size": "a4", "margin_mm": 10}'
Bigger than ~100 MB in total? Upload each file first (POST /api/v1/uploads),
then send the URLs in order:
{ "service_slug": "docconvert", "operation_key": "merge_pdfs",
"params": { "file_urls": ["https://.../a.pdf", "https://.../b.pdf"] } }
Limits and errors:
- one job takes up to 30 files by default (an admin setting;
400 too many files for one job — up to N); one job is one charge however many files; file_urlsmust be files uploaded through this site (400 file_urls must be files uploaded through this site); each one is kind-checked (400 … file 2 looks like image);- a single-file operation refuses
file_urls(400 'compress_pdf' takes one file — send file_url, not file_urls); - a password-protected PDF fails with
this PDF is password-protected — unlock it first; a page range outside the document names the valid range; pdf_to_textreads the PDF's text layer — a scan without one fails withthis PDF has no text layer (a scan?). There is no OCR.