1. Quickstart — 2 requests
Request 1 — send the file and what to do with it
One multipart call: the file plus the operation. Returns a job.
curl -s https://metaplatho.com/api/v1/jobs/upload \
-H 'X-Api-Key: ca_live_...' \
-F 'file=@/path/to/photo.jpg' \
-F 'service_slug=photoconvert' \
-F 'operation_key=compress' \
-F 'params={"quality": 70, "format": "jpeg"}'
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "processing",
"operation_key": "compress",
"estimated_cost": 1,
"result": null,
"error": null
}
Copy the id.
Form fields:
| Field | Required | What it is |
|---|---|---|
file |
yes* | the file itself |
files |
— | *instead of file: several files for ONE job, in order (repeat the field) — merge_pdfs, images_to_pdf |
service_slug |
yes | photoconvert (images), clipconvert (video) or docconvert (PDF) |
operation_key |
yes | what to do — see the catalog |
params |
— | a JSON object as a string — the operation's settings |
webhook_url |
— | public URL we POST the finished job to (then you can skip request 2) |
high_priority |
— | accepted for compatibility; the job already runs at your plan's priority |
file_url is filled in from the uploaded file — if you put one in params it is
ignored.
⚠️ The one mistake everybody makes:
paramsis a string
paramsis a multipart form field, so its value must be JSON already serialised to text. Passing an object makes your HTTP client stringify it as[object Object](JS) or{'quality': 70}with single quotes (Python) — the server then rejects it with400 params must be a JSON object.// ✅ correct form.append("params", JSON.stringify({ quality: 70, format: "jpeg" })); // ❌ wrong — sent as [object Object] form.append("params", { quality: 70, format: "jpeg" });# ✅ correct data = {"service_slug": "photoconvert", "params": json.dumps({"quality": 70})} # ❌ wrong — Python dict repr uses single quotes, which is not JSON data = {"service_slug": "photoconvert", "params": {"quality": 70}}In curl it is already a string, so
-F 'params={"quality":70}'is fine.
Which operations exist, and which files they accept
service_slugandoperation_keyare a pair. Each key is registered under one service and declares the media kinds it takes (accepts— usually one;transcribetakes video and audio):photoconvert+compressfor images,clipconvert+mutefor video. Use a key from the wrong service and you get{ "detail": "unknown operation 'mute' for photoconvert — valid: circle, compress, convert, crop, … (params of each: GET /api/v1/studio/tools)" }with
400— even thoughmuteis a real operation, just not onphotoconvert. The catalog lists every valid pair, andGET https://metaplatho.com/api/v1/studio/toolsreturns the same list with anacceptsfield (["image"]/["video"]) so you can validate before submitting. Each entry'sconfig.keywordsis the search vocabulary for that tool — synonyms and intents in en/uk/ru ("smaller", "сжать", "зменшити" → compress) — the same list the studio's search box uses, so a client that offers its own tool picker can search the way the studio does.config.popular: truemarks the tiles behind the studio's "Popular" filter.
Which upload path do I use?
file ≤ 100 MB → POST /api/v1/jobs/upload (this page, one call)
file > 100 MB → POST /api/v1/uploads/presign (or /uploads/multipart/*)
up to your tier's cap PUT the bytes to the returned put_url
(6 GB on paid plans)
POST /api/v1/jobs/api with params.file_url
file is on your → POST /api/v1/upload-sessions/api
END USER's device send them the returned upload_url
(Telegram bots) (see section 7)
Exact numbers: the CDN caps a proxied request body at ~100 MB, so anything
larger must go straight to storage. How big one file may be depends on your
tier — 300 MB anonymous, 1 GB free account, 6 GB paid plans (live values:
GET /api/v1/public/config → billing.input_max_mb); the platform's outer edge
is MAX_UPLOAD_GB = 6 GB. A bigger file is refused with 413 when the upload
is opened, before a byte is stored. Multipart uses 64 MB parts, up to 10 000
of them. Upload links live 1 hour by
default, 24 h maximum. See Big files.
Request 2 — check the status and grab the result
curl -s https://metaplatho.com/api/v1/jobs/6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42 \
-H 'X-Api-Key: ca_live_...'
Repeat every 2–3 seconds until status stops being queued or processing.
Job lifecycle — three of the five states are terminal:
queued ──► processing ──┬──► done result.output_url is ready
├──► failed error explains why — refunded (rule in the table below)
└──► canceled you canceled it — refunded the same way
status |
Terminal? | Meaning |
|---|---|---|
queued |
no | accepted, not started yet — keep polling |
processing |
no | running — keep polling |
done |
yes | finished — the file is at result.output_url |
failed |
yes | see error; refunded — final_cost is 0, or the base if the worker had it over 60 s |
canceled |
yes | you canceled it; refunded the same way |
The response shape is the same in every state; only status, result and
error change. All four, in full:
Still working — result and error are both null:
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "processing",
"service_id": 3,
"operation_key": "compress",
"params": { "quality": 70, "file_url": "https://.../photo.jpg" },
"result": null,
"error": null,
"estimated_cost": 1,
"final_cost": null,
"created_at": "2026-07-22T12:00:00Z",
"finished_at": null
}
The id, status, result and error fields are the only ones an integration
needs; parse defensively and ignore any field you don't recognise. On done,
result.outputs[] lists every file the job made and result.body is the
operation's own report (output_format, output_size_bytes, language, for
video encode_mode…). params echoes what you sent plus file_url (the stored
input) and __filename__ (the original name, for your bookkeeping).
Two more fields appear on the single-job reads (POST /jobs, POST /jobs/api,
POST /jobs/upload, GET /jobs/{id}, POST /jobs/{id}/repeat,
POST /jobs/{id}/cancel):
queue_position— your place in line whilestatusisqueued, where 1 is next to be handed to a worker.nullfor every other status, andnullon theGET /jobslisting. It is the real release order — higher tiers go first, then by when each wait ends — so it can go up if paid work arrives behind you.input_duration_sec— the length of the input media in whole seconds, as probed by the worker.nulluntil it is reported, and alwaysnullfor operations that are not priced by length. This is the number a speech-to-text bill is computed from.
Done — take result.output_url:
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "done",
"operation_key": "compress",
"result": { "output_url": "https://.../out/abc123/photo.jpg" },
"error": null,
"estimated_cost": 1,
"final_cost": 1,
"finished_at": "2026-07-22T12:00:04Z"
}
Failed — read error; refunded (final_cost is what, if anything, was kept):
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "failed",
"result": null,
"error": "could not start job: unsupported input format",
"estimated_cost": 1,
"final_cost": 0,
"finished_at": "2026-07-22T12:00:02Z"
}
Canceled — via POST /api/v1/jobs/{id}/cancel:
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "canceled",
"result": null,
"error": null,
"final_cost": 0,
"finished_at": "2026-07-22T12:00:03Z"
}
Treat any status you do not recognise as non-terminal and keep polling — new
states would only ever be added between queued and the terminal three.
result.output_url is a plain public URL — download it directly:
curl -L -o result.jpg "https://.../out/abc123/photo.jpg"
Every file a job made is in result.outputs. output_url is the main
file; outputs lists all of them, each with role, kind (video, image,
audio, subtitles, text, json, archive), format, url, filename
and size_bytes. A captioned video comes back as the video and its
captions as a separate SRT file, so you can edit the text and burn it again:
"result": {
"output_url": "https://.../out/af46/clip.mp4",
"outputs": [
{ "role": "result", "kind": "video", "format": "mp4", "url": "https://.../out/af46/clip.mp4", "filename": "clip.mp4", "size_bytes": 8123456 },
{ "role": "captions", "kind": "subtitles", "format": "srt", "url": "https://metaplatho.com/api/v1/jobs/<id>/captions.srt", "filename": "clip.srt", "size_bytes": 912 }
],
"body": { "output_format": "mp4", "language": "en", "subtitles_srt": "1\n00:00:00,000 --> …" }
}
result.body holds what the operation reports about its run (language,
duration, size, the inline SRT…). Every url downloads without the API key.
In Postman: paste the URL, Send, then Save Response → Save to a file.
The whole flow as a collection: GET https://metaplatho.com/api/v1/public/postman — Postman →
Import → paste this link; set mp_api_key once (Variables tab), pick your file,
Send (request 2 polls on its own and hands request 3 the output_url).
queuedcan last a while. Start time is not guaranteed: depending on your plan a job may be held before processing begins (lower tiers wait, and can also wait for higher-priority work to finish). Nothing is wrong — keep polling, or use a webhook. Never resubmit aqueuedjob: the second submit is a new job and is charged again.
That's the whole API. Everything below is reference: which operations exist, what parameters each one takes, and the extra routes for big files.
Changing what happens — only params changes
Same two requests, different settings. Crop a photo to a square:
curl -s https://metaplatho.com/api/v1/jobs/upload -H 'X-Api-Key: ca_live_...' \
-F 'file=@/path/to/photo.jpg' \
-F 'service_slug=photoconvert' -F 'operation_key=crop' \
-F 'params={"crop_aspect": "1:1"}'
Burn auto-captions onto a video:
curl -s https://metaplatho.com/api/v1/jobs/upload -H 'X-Api-Key: ca_live_...' \
-F 'file=@/path/to/clip.mp4' \
-F 'service_slug=clipconvert' -F 'operation_key=subtitles' \
-F 'params={"style": "karaoke", "position": "top"}'
Check your key (optional)
curl -s https://metaplatho.com/api/v1/me/ping -H 'X-Api-Key: ca_live_...'
# -> {"ok":true,"token_label":"my-app","plan":"free","credits":98}
Skip polling with a webhook
Add -F 'webhook_url=https://my-app.example.com/hooks/job-done' to request 1 and
we POST the job there once it is done, failed or canceled — the same
object GET /api/v1/jobs/{id} returns, outputs included, with the header
X-MetaPlatho-Event: job.done (job.failed, job.canceled). Must be a public
http(s) URL — private and internal addresses are rejected with 422.
Answer 2xx. A 5xx, 429 or timeout is retried twice (after 5 s and 30 s);
any other 4xx is taken as your answer and not retried; redirects are not
followed. The POST is unsigned — anyone who knows the URL could send one —
so if you act on money or access, re-read the job with GET /api/v1/jobs/{id}
rather than trusting the body. Delivery is best effort: keep a slow poll as the
backstop.