2. Integrate with an AI agent (Claude Code, Cursor, ChatGPT…)
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.
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)
401 missing/invalid API key 402 not enough credits
404 unknown service or job 413 file too large 415 file type not allowed
422 validation error (e.g. webhook_url pointing at a private address)
429 rate limited — HONOR the Retry-After response header (seconds)
5xx retry with exponential backoff
BILLING: credits are debited at submit and refunded automatically if the job fails or is
canceled. The price is the `credits_per_call` of the operation you address — and, if you send a
`params.operations` pipeline, of the dearest operation in it, because that list is what actually
runs. 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 every available operation with: service_slug, operation_key, label, group,
accepts (["image"] or ["video"]), credits_per_call, presets[].params (ready-made
settings) and config.fields (each tunable parameter with type/min/max).
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.