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: params is a string

params is 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 with 400 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_slug and operation_key are a pair. Each key is registered under one service and accepts one media kind: photoconvert + compress for images, clipconvert + mute for video. Use a key from the wrong service and you get

{ "detail": "unknown operation 'mute'" }

with 400 — even though mute is a real operation, just not on photoconvert. The catalog lists every valid pair, and GET https://mp.dvocorp.com/api/v1/studio/tools returns the same list with an accepts field (["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.

queued can 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 a queued job: 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.