2. Integrate with an AI agent (Claude Code, Cursor, ChatGPT…)
Two ways, pick one:
- Connect the MCP server — one command, and the agent drives the API through tools (below). Use this when the agent itself should process files for you, or should build and test an integration against a live account.
- Paste the prompt (further down) — a self-contained spec of the REST API for agents that cannot connect to an MCP server, or when you want plain REST code with no runtime dependency.
Connect the MCP server (Claude Code, Codex, Cursor, Gemini CLI, VS Code…)
The server speaks MCP over Streamable HTTP at https://mp.dvocorp.com/api/mcp and takes the
same key as the REST API, in either header:
Authorization: Bearer ca_live_... # or
X-Api-Key: ca_live_...
The panel's Developer API page (/api) shows every command below with your
key already filled in. The key goes into your local agent config — where
that agent keeps its other tokens — never into a chat.
# Claude Code
claude mcp add --transport http mediaprocessing https://mp.dvocorp.com/api/mcp \
--header "Authorization: Bearer $API_KEY"
# Codex (the token is read from an env var; Codex has no header flag)
export MP_API_KEY=ca_live_...
codex mcp add mediaprocessing --url https://mp.dvocorp.com/api/mcp --bearer-token-env-var MP_API_KEY
# Gemini CLI
gemini mcp add --transport http --header "Authorization: Bearer $API_KEY" mediaprocessing https://mp.dvocorp.com/api/mcp
// Cursor (~/.cursor/mcp.json), Windsurf and most other clients
{ "mcpServers": { "mediaprocessing": {
"url": "https://mp.dvocorp.com/api/mcp",
"headers": { "Authorization": "Bearer ca_live_..." } } } }
// VS Code (.vscode/mcp.json)
{ "servers": { "mediaprocessing": {
"type": "http", "url": "https://mp.dvocorp.com/api/mcp",
"headers": { "Authorization": "Bearer ca_live_..." } } } }
// OpenAI Responses API — your own software, server-side
{ "model": "gpt-5",
"tools": [{ "type": "mcp", "server_label": "mediaprocessing",
"server_url": "https://mp.dvocorp.com/api/mcp",
"headers": { "Authorization": "Bearer ca_live_..." },
"require_approval": "never" }],
"input": "compress https://example.com/demo.mp4 for Telegram" }
Then tell the agent what you need — "compress ./demo.mp4 for Telegram and give me the link" — or "add this integration to my bot".
The tools
| Tool | What it does |
|---|---|
ping() |
Checks the key and the balance. Spends nothing. |
list_operations(kind?) |
The live catalog: every service_slug / operation_key, what media it accepts, input (file or url), credits_per_call, fields (each param's type/min/max) and presets. kind = image | video | audio narrows it. |
submit_job(service_slug, operation_key, file_url?, params?, webhook_url?) |
Starts a job. file_url is the input for file operations; link operations take the link in params.url. Debits at submit, refunds on failure — the same path as POST /jobs/api. |
get_job(job_id) |
Status, output_url once done, error once failed, estimated_cost / final_cost, queue position. |
wait_for_job(job_id, timeout_seconds=60) |
Polls for you, up to 120 s per call; timed_out: true means call again. |
list_jobs(limit=20) |
Your recent jobs, newest first. |
presign_upload(filename, content_type) |
A slot for a file on the agent's machine: PUT the bytes to put_url (the answer includes the curl -T line), then pass url as file_url. Multi-GB is fine; the bytes never pass through the API. |
create_upload_link(service_slug, operation_key, params?, callback_url?, metadata?, …) |
A one-time upload page for a file your end user holds (Telegram bots): the same body as POST /upload-sessions/api, section 7. |
get_upload_link(session_id) |
The link's state and the jobs it produced. |
Every call goes through the same code as the REST route it mirrors: the
same plan quota, per-key daily cap, in-flight cap, maintenance window, billing
and usage journal. The MCP server adds no prices and no limits of its own.
Errors come back as tool errors carrying the REST status and detail
(402: insufficient credits, 429: … (retry after 60s)).
The server is stateless: every JSON-RPC call is self-contained, which is
what lets it sit behind a load balancer with no sticky sessions. A missing or
bad key is a plain HTTP 401 (a disabled account 403), not a JSON-RPC
error — so claude mcp add tells you right away.
An admin can switch MCP access off per audience (feature
mcp, on by default) without touching the REST API.
The prompt (for agents without MCP)
Copy the block below, paste it into your AI coding agent and say "here is the spec, add this integration". It is a complete, self-contained contract — the agent needs nothing else from this page.
Do not put your API key in the prompt. The block deliberately uses an env var. Pasting a live credential into a third-party chat leaks it into that vendor's logs; the agent only needs to know the key's name.
You are integrating the Media Processing API (image & video processing) into this project.
BASE URL: https://mp.dvocorp.com
AUTH: every request sends the header X-Api-Key: <key>
The key looks like ca_live_... . Read it from the MP_API_KEY env var.
Never hardcode it, never log it, never commit it.
THE WHOLE API IS TWO REQUESTS.
1) SUBMIT — one multipart POST carrying the file AND what to do with it:
POST https://mp.dvocorp.com/api/v1/jobs/upload
header: X-Api-Key: <key>
multipart/form-data fields:
file (required) the binary file
service_slug (required) "photoconvert" for images, "clipconvert" for video
operation_key (optional) what to do — see DISCOVERY
params (optional) the operation's settings as a JSON STRING, e.g. {"quality":70}
webhook_url (optional) public http(s) URL; the finished job is POSTed there
high_priority (optional) "true" — honored on premium plans, silently ignored otherwise
-> 201 {"id":"<uuid>","status":"processing","estimated_cost":1,"result":null,"error":null}
THE #1 INTEGRATION BUG — params is a STRING, not an object. It is a multipart
form field, so serialise it yourself:
JS: form.append("params", JSON.stringify({quality: 70})) correct
form.append("params", {quality: 70}) WRONG, sends [object Object]
Python: data={"params": json.dumps({"quality": 70})} correct
data={"params": {"quality": 70}} WRONG, dict repr is not JSON
The server answers 400 "params must be a JSON object" when you get this wrong.
service_slug and operation_key are a PAIR, and each operation accepts one media
kind — check "accepts" in the catalog before submitting. A video operation on an
image is a 400 whose detail names BOTH kinds, e.g.
"this operation works on video files — the file you sent looks like image"
An unknown operation_key for that service is 400 "unknown operation '<key>'".
Do NOT put file_url in params: the server injects it from the uploaded file and it always wins.
Do NOT set Content-Type yourself; let the HTTP client set the multipart boundary.
2) POLL — until the status is terminal:
GET https://mp.dvocorp.com/api/v1/jobs/{id} (same X-Api-Key header)
status "queued" | "processing" -> keep polling, every 2-3 seconds
status "done" -> result.output_url is a plain public URL; download it directly
status "failed" -> read `error`; the credits were refunded automatically
status "canceled" -> credits refunded
If you set webhook_url, skip polling entirely: the same job object is POSTed there.
TELEGRAM BOTS AND ANY "MY END USER HAS THE FILE" INTEGRATION:
A Telegram bot CANNOT relay big media: the Bot API lets a bot download only
~20 MB via getFile and send only ~50 MB. That limit cannot be worked around
from the bot side — do not try to stream the file through your bot. Mint a
one-time upload link instead and let the user's browser upload straight to
storage, bypassing both Telegram's servers and ours.
POST https://mp.dvocorp.com/api/v1/upload-sessions/api header: X-Api-Key: <key>
{
"service_slug": "clipconvert",
"operation_key": "compress",
"params": {"level": "medium"},
"expires_in_seconds": 3600,
"max_file_size_bytes": 2147483648,
"allowed_mime_types": ["video/mp4", "video/quicktime"],
"callback_url": "https://my-bot.example.com/hooks/mp",
"delete_after_processing": true,
"metadata": {"telegram_user_id": "123", "chat_id": "456", "request_id": "abc"}
}
-> 201 {"id":"<session_id>", "upload_url":"https://mp.dvocorp.com/u/<token>",
"expires_at":"...", "estimated_cost":3,
"callback_secret":"<shown once, only when the server generated it>"}
Send upload_url to the user and stop. The page it opens already does
presigned multipart upload with per-part retry, a progress bar and cancel —
you do not build an uploader.
Fields that matter for a bot:
metadata opaque JSON, echoed back VERBATIM in every callback.
This is how you map a finished file back to a chat:
put telegram_user_id / chat_id / your request_id here.
expires_in_seconds 60..604800, default 1h, server-capped at 24h.
max_files 1..20 (default 1); every file runs every operation.
operations[] up to 10 {service_slug, operation_key, params} — one
job per entry on the same file. Mutually exclusive
with the single service_slug/operation_key/params.
bundle_zip true -> the owner can pull one .zip of everything.
max_file_size_bytes your own cap; the platform cap (3 GB) still applies.
allowed_mime_types anything else is rejected with 415.
delete_after_processing true -> the uploaded SOURCE is deleted as soon as
processing succeeds.
Unknown operation (404) and insufficient credits (402) are reported at CREATE
time — before the user ever opens the link. Handle them there.
ONE-TIME AND EXPIRING BY DESIGN: a second upload to the same link gets 409,
an expired link gets 410. Both are final — mint a new session. On your side,
store request_id -> session_id so a retried bot command does not mint a
second link and charge twice.
RESULTS FROM AN UPLOAD LINK — webhook or poll:
With callback_url set, these events are POSTed to it:
upload.completed the file landed and was verified
processing.completed all linked jobs finished, at least one succeeded
processing.failed they all failed
Headers: X-BigLoader-Event: <event>
X-BigLoader-Signature: hex HMAC-SHA256(callback_secret, RAW body)
Verify the HMAC over the RAW request body BEFORE parsing JSON. Delivery is
best-effort, so keep polling as a fallback.
Body: {"event","session_id","status","file":{...},"job_id","job_ids",
"metadata":<yours, verbatim>,"jobs":[{"id","operation_key","status",
"output_url","error"}],"result":{"output_url"}}
GET https://mp.dvocorp.com/api/v1/upload-sessions/{session_id} status + jobs + results
created -> uploading -> uploaded -> processing -> done
(plus failed, expired) — coarse upload progress comes from here
GET https://mp.dvocorp.com/api/v1/upload-sessions your recent sessions
GET https://mp.dvocorp.com/api/v1/upload-sessions/{id}/archive .zip of all uploads
DELETE https://mp.dvocorp.com/api/v1/upload-sessions/{id} deletes the files and
cancels + refunds any in-flight job
SENDING THE RESULT BACK: result.output_url is a plain public URL. If the
processed file exceeds Telegram's ~50 MB send limit, send the URL as a
message instead of uploading the file. A round video note (operation "circle"
with format "note", <=60s) only renders round when a bot calls sendVideoNote
with an uploaded file.
FILES OVER ~100 MB THAT YOUR OWN SERVER HOLDS (the CDN caps request bodies) —
three steps instead of one:
a) POST https://mp.dvocorp.com/api/v1/uploads (multipart, field `file`) -> {"url": "..."}
for multi-GB: POST https://mp.dvocorp.com/api/v1/uploads/presign -> PUT the bytes to put_url
b) POST https://mp.dvocorp.com/api/v1/jobs/api (JSON body, not multipart):
{"service_slug":"...","operation_key":"...","params":{"file_url":"<url from a>"}}
c) poll exactly as in step 2.
ERRORS — the body is {"detail": ...}: a string, or a list of field errors for 422.
400 bad request — unknown operation, invalid file URL, wrong media kind, OR a
param the operation refused. A bad param NAMES the field and refunds any charge:
"could not start the job — quality: Input should be less than or equal to 100 (charge refunded)"
401 missing/invalid API key ("missing credentials") 402 not enough credits
404 unknown service or job 413 file too large 415 file type not allowed
422 validation error — pydantic detail array; e.g. webhook_url pointing at a
private address, or ?limit outside 1..200 on GET /jobs
429 rate limited — HONOR the Retry-After response header (seconds)
5xx retry with exponential backoff
BILLING: ONE tariff, the same for every operation, measured in processing time. 2 credits per
file are debited at submit, then 1 more for each further STARTED minute of processing beyond the
first, never more than 30 for one file. So <=60s costs 2, 61-120s costs 3, 121-180s costs 4. The
operation you address does not change the price, and neither does a `params.operations` pipeline.
Credits are refunded automatically if the job fails or is canceled, and a failed or canceled job
is never charged for time. An input longer than your tier's length limit is refused and refunded
in full (10 / 30 / 180 min for anonymous / free / paid). Read the live numbers from
GET /api/v1/public/config -> billing.time_tariff_*. GET https://mp.dvocorp.com/api/v1/me/ping ->
{"ok":true,"plan":"free","credits":98} is a cheap credential + balance check.
DISCOVERY — never hardcode the operation list:
GET https://mp.dvocorp.com/api/v1/studio/tools (public, no auth)
Returns a JSON array; every entry is one operation with: operation_id, service_slug,
operation_key, label{en,uk,ru}, group, accepts (["image"] or ["video"]),
credits_per_call, config.fields[] (each tunable param with key/type/min/max) and
config.presets[] (ready-made settings, each {label, params}). NOTE: the top-level
presets[] carries only {index, label} — the actual ready-made params are under
config.presets[].params. Fetch this once and derive your operation list + validation
from it.
WHAT TO BUILD — a small typed client. These are DIFFERENT paths; do not collapse
them into one submit():
submit(file, serviceSlug, operationKey, params) -> jobId
multipart POST /jobs/upload. Files up to ~100 MB that your process holds.
submitLarge(pathOrStream, serviceSlug, operationKey, params) -> jobId
presign (or multipart create/sign-part/complete) -> PUT the bytes ->
POST /jobs/api with params.file_url. Multi-GB files your process holds.
createUploadLink({operation, params, metadata, ttl, maxFileSizeBytes,
allowedMimeTypes, callbackUrl, deleteAfterProcessing})
-> {uploadUrl, sessionId, expiresAt}
For files your END USER holds (Telegram bots). You never touch the bytes.
waitForResult(jobId) -> outputUrl poll GET /jobs/{id}
getSession(sessionId) -> {status, jobs, metadata}
verifyCallback(rawBody, signatureHeader, secret) -> boolean
HMAC-SHA256 over the RAW body; use a constant-time compare.
listOperations() from /studio/tools
Also: retry on 429/5xx honoring Retry-After; persist your own request_id ->
session_id / job_id so a retried command never charges twice; keep the
credential in the environment. Ask me before adding a dependency.
Getting the operation list into the prompt
The block above deliberately tells the agent to discover operations at runtime instead of pasting 40 of them. If your agent has no network access, fetch the catalog yourself and hand it over as a file:
curl -s https://mp.dvocorp.com/api/v1/studio/tools > operations.json
Then: "use operations.json as the operation catalog". The panel's Developer
API page (/api) also has a Copy AI prompt button that embeds a current
snapshot of the catalog for you.
Checking what the agent built
Ask it to prove the integration works against a real job:
Run the integration end to end: submit a small test image with
service_slug=photoconvert, operation_key=compress, params={"quality":70},
poll until done, download result.output_url, and show me the job id,
the final status and the output file size.
If that produces a downloaded file, the integration is correct.