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 }; 503 when 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/config exposes telegram_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 from GET /api/v1/studio/tools) or a manual section update, and (c) en/uk/ru strings in frontend/src/i18n/translations.ts. PRs without docs are incomplete.