10. Appendix
Ready-made scripts (bash / Node / Python)
You don't need these — the two requests above are the whole API. These just wrap them in a polling loop.
bash
#!/usr/bin/env bash
set -euo pipefail
BASE="https://mp.dvocorp.com"
API_KEY="ca_live_..."
JOB=$(curl -s "https://mp.dvocorp.com/api/v1/jobs/upload" -H "X-Api-Key: $API_KEY" \
-F '[email protected]' -F 'service_slug=photoconvert' \
-F 'operation_key=compress' -F 'params={"quality":70}')
JOB_ID=$(printf '%s' "$JOB" | jq -r .id)
STATUS=processing
until [ "$STATUS" = done ] || [ "$STATUS" = failed ] || [ "$STATUS" = canceled ]; do
sleep 2
RESP=$(curl -s "https://mp.dvocorp.com/api/v1/jobs/$JOB_ID" -H "X-Api-Key: $API_KEY")
STATUS=$(printf '%s' "$RESP" | jq -r .status)
done
printf '%s' "$RESP" | jq -r '.result.output_url // .error'
Node 18+ — save as run.mjs, then node run.mjs
import { readFile } from "node:fs/promises";
const BASE = "https://mp.dvocorp.com";
const API_KEY = process.env.MP_API_KEY;
const form = new FormData();
form.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
form.append("service_slug", "photoconvert");
form.append("operation_key", "compress");
form.append("params", JSON.stringify({ quality: 70 }));
const job = await fetch(`${BASE}/api/v1/jobs/upload`, {
method: "POST",
headers: { "X-Api-Key": API_KEY },
body: form,
}).then(r => r.json());
let st = job;
while (st.status === "queued" || st.status === "processing") {
await new Promise(r => setTimeout(r, 2000));
st = await fetch(`${BASE}/api/v1/jobs/${job.id}`, {
headers: { "X-Api-Key": API_KEY },
}).then(r => r.json());
}
if (st.status !== "done") throw new Error(`job ${st.status}: ${st.error}`);
console.log(st.result.output_url);
Python 3
import time, requests
BASE = "https://mp.dvocorp.com"
H = {"X-Api-Key": "ca_live_..."}
job = requests.post(
f"{BASE}/api/v1/jobs/upload",
headers=H,
files={"file": open("photo.jpg", "rb")},
data={
"service_slug": "photoconvert",
"operation_key": "compress",
"params": '{"quality": 70}',
},
).json()
st = job
while st["status"] in ("queued", "processing"):
time.sleep(2)
st = requests.get(f"{BASE}/api/v1/jobs/{job['id']}", headers=H).json()
assert st["status"] == "done", f"job {st['status']}: {st.get('error')}"
print(st["result"]["output_url"])
Studio endpoints (operation_id-addressed alternative)
POST https://mp.dvocorp.com/api/v1/studio/run and POST https://mp.dvocorp.com/api/v1/studio/batch accept an
API key and are what the web studio uses. They address operations by numeric
operation_id (read it from GET /studio/tools) rather than by
service_slug + operation_key, and they only accept files uploaded through
this site — an external file_url is rejected with 400.
For most integrations POST /jobs/upload is the better entry point. Use
/studio/batch when you want one call to fan out over many files and give you a
single result_zip_url:
curl -s https://mp.dvocorp.com/api/v1/studio/batch \
-H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
-d '{"operation_id": 42, "file_urls": ["https://.../a.jpg", "https://.../b.jpg"],
"naming": "sequential", "params": {"quality": 70}}'
naming: keep / sequential / random. Poll
GET https://mp.dvocorp.com/api/v1/studio/batches/{batch_id} for status, done, failed and
result_zip_url.
POST https://mp.dvocorp.com/api/v1/studio/batches/{batch_id}/cancel stops a running batch and
refunds the items that never ran (409 if it already finished).
POST https://mp.dvocorp.com/api/v1/studio/batches/{batch_id}/repeat re-runs the same sources
with the same operation and returns a new batch, billed as a fresh submit.
Both take X-Api-Key.
sources mirrors your submitted URLs in the same order. Video batches also
carry per-file results as they finish, so you can preview or fetch any single
output without unpacking the ZIP: params._items is a list of
{idx, name, url} (idx = position in your file_urls). Image batches do not
return _items — take result_zip_url. For video pipelines that
recognise speech, items of small batches (≤12 files) additionally include
srt — the captions of exactly that video, ready for an editor without a
second transcription pass.
Telegram video-note delivery (first-party panel feature)
Telegram only produces a round кружок when a bot calls sendVideoNote with
an uploaded file — a downloaded square mp4 sent normally stays square. So the
studio hands the user a deep link into one of our delivery bots, and the bot
re-uploads the note.
This is a panel feature, not an API-key operation:
POST https://mp.dvocorp.com/api/v1/tg/deliver— body{ result_url }or{ job_id }, plus optional{ bot_id }. Returns{ deep_link, token, bot_id };503when no active bot exists.POST https://mp.dvocorp.com/api/v1/tg/webhook/{bot_id}— the per-bot Telegram update sink. Not for clients.GET https://mp.dvocorp.com/api/v1/public/configexposestelegram_bots: [{id, name, username}]so the SPA can show the button.
Admin setup: create a bot with @BotFather, then in the admin panel go to
Services → Telegram bots → Add bot (name, @username, token, mark
Active). On save the panel registers the webhook at
{TELEGRAM_WEBHOOK_BASE or APP_BASE_URL}/api/v1/tg/webhook/<bot_id> — the host
must be publicly reachable by Telegram. Bots are DB-managed; the feature is on
as soon as one active bot with a token exists.
Other public endpoints
| Request | Auth | Returns |
|---|---|---|
GET https://mp.dvocorp.com/api/v1/health |
none | liveness |
GET https://mp.dvocorp.com/api/v1/studio/tools |
none | the operations catalog |
GET https://mp.dvocorp.com/api/v1/public/config |
none | {anon_enabled, anon_allow_heavy, anon_allow_batch, registration_enabled, studio_tiles_v2, telegram_bots, telegram_enabled, marketing} — these govern the no-login web studio only; API-key access is unaffected. studio_tiles_v2 is presentation only (true = the icon tile grid, false = the classic text tiles; admin-switchable under Settings) and never changes what an operation does. marketing carries the web app's own ad-counter ids (ga4_id, google_ads_id, google_ads_signup_label, google_ads_purchase_label, meta_pixel_id, consent_required) — browser-only, irrelevant to API clients, and empty unless the owner configured a counter |
GET https://mp.dvocorp.com/api/v1/uploads/download?url=… |
none | streams an allow-listed storage URL with an attachment header (rate limited per account, or per IP when called anonymously) — a browser convenience, not needed for API clients |
GET https://mp.dvocorp.com/api/docs |
none | interactive OpenAPI docs |
GET https://mp.dvocorp.com/api/openapi.json |
none | the raw spec |
Usage analytics are recorded automatically for the admin dashboards; there is no API-key analytics endpoint. See docs/ANALYTICS.md.
Maintainer note — new feature → docs in the same change. Every new user-facing operation or API endpoint MUST ship in the same change with: (a) an update to this file, (b) the client-panel Developer API page (
frontend/src/pages/ApiPage.tsx, route/api) — automatically via the studio tools registry when possible (register the op with studio labels/groups/presets; the catalog renders fromGET /api/v1/studio/tools) or a manual section update, and (c) en/uk/ru strings infrontend/src/i18n/translations.ts. PRs without docs are incomplete.