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://mp.dvocorp.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 |
service_slug |
yes | photoconvert (images) or clipconvert (video) |
operation_key |
— | 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 |
— | true — honored on premium plans, silently ignored otherwise |
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 accepts one media kind:photoconvert+compressfor images,clipconvert+mutefor video. Use a key from the wrong service and you get{ "detail": "unknown operation 'mute'" }with
400— even thoughmuteis a real operation, just not onphotoconvert. The catalog lists every valid pair, andGET https://mp.dvocorp.com/api/v1/studio/toolsreturns the same list with anacceptsfield (["image"]/["video"]) so you can validate before submitting.
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 3 GB PUT the bytes to the returned put_url
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. The platform cap is MAX_UPLOAD_GB = 3 GB;
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://mp.dvocorp.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, credits refunded
└──► canceled you canceled it, credits refunded
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; credits were refunded |
canceled |
yes | you canceled it; credits refunded |
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",
"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
}
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; you were not charged:
{
"id": "6f9c1c9e-2b40-4a71-9c3e-1d5a8f0b7e42",
"status": "failed",
"result": null,
"error": "could not start job: unsupported input format",
"estimated_cost": 1,
"final_cost": null,
"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": null,
"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"
In Postman: paste the URL, Send, then Save Response → Save to a file.
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://mp.dvocorp.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://mp.dvocorp.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://mp.dvocorp.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 finished job object there. Must be a public http(s) URL — private
and internal addresses are rejected with 422.