A video is made by a run. You start a run, check on it until it finishes, then download the MP4. The steps below make one script: paste the setup, then step 1 (or its pasted-text version), then steps 2 and 3, in the same shell session or file. Pick a language once and every sample on the page switches to it.
Put your key in the VIDONTO_API_KEY environment variable and set the base URL. The other steps use these names.
# Needs curl and jq.
export VIDONTO_API_KEY="qsv_..." # from the API keys page
API="https://www.vidonto.com/api/v1"
// Node.js 18 or later, as an ES module (for example quickstart.mjs).
// Run with VIDONTO_API_KEY set in the environment.
import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const API = "https://www.vidonto.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.VIDONTO_API_KEY}`,
"Content-Type": "application/json",
};
# pip install requests. Run with VIDONTO_API_KEY set in the environment.
import os
import time
import uuid
import requests
API = "https://www.vidonto.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VIDONTO_API_KEY']}"
Send a YouTube video or web page link as url. Every other field is optional; anything you leave out comes from your saved settings (see Options). The Idempotency-Key header makes it safe to repeat the request if the network drops.
RUN=$(curl -s -X POST "$API/runs" \
-H "Authorization: Bearer $VIDONTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"targetDuration": 60,
"videoFormat": "vertical"
}')
echo "$RUN"
# Keep the new run's id for the next steps (empty if the request was refused).
RUN_ID=$(echo "$RUN" | jq -r '.id // empty')
const response = await fetch(`${API}/runs`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": randomUUID() },
body: JSON.stringify({
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
targetDuration: 60,
videoFormat: "vertical",
}),
});
let run = await response.json();
if (!response.ok) throw new Error(`${run.error.code}: ${run.error.message}`);
console.log(run.id, run.status); // "3f0c6b6e-…" "running"
response = session.post(
f"{API}/runs",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"targetDuration": 60,
"videoFormat": "vertical",
},
)
run = response.json()
if not response.ok:
raise RuntimeError(f"{run['error']['code']}: {run['error']['message']}")
print(run["id"], run["status"]) # 3f0c6b6e-… running
The API answers 202 Accepted with the new run, and the sample keeps it (or its RUN_ID in the shell) for the next steps. Its estimated credits are now held from your balance (see Credits).
{
"id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
"status": "running",
"stage": "transcript",
"progress": 0,
"error": null,
"retryable": false,
"title": null,
"source": {
"kind": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
},
"options": {
"targetDuration": 90,
"summaryModel": "gpt-5.6-terra",
"videoFormat": "landscape",
"textMode": "narration",
"captionLength": "short",
"aiImages": false,
"voiceId": "21m00Tcm4TlvDq8ikWAM",
"voiceModel": "eleven_v4",
"transcriptLanguage": "en",
"openingStyle": "coming_up",
"endingStyle": "none",
"customSignOff": null,
"creditSource": false,
"instructions": null
},
"stages": [
{
"id": "transcript",
"label": "Fetching transcript",
"status": "running",
"progress": 0
},
{
"id": "summary",
"label": "Script",
"status": "pending",
"progress": 0
},
{
"id": "storyboard",
"label": "Storyboard",
"status": "pending",
"progress": 0
},
{
"id": "render",
"label": "Voiceover & render",
"status": "pending",
"progress": 0
}
],
"chargedCredits": 0,
"video": null,
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
Use this instead of step 1 to summarize your own text from a file, here article.txt. Send text instead of url, with an optional title; send one or the other, never both. Use textMode "key_points" to show the main points on screen instead of the default spoken "narration".
RUN=$(jq -n --rawfile text article.txt \
'{text: $text, title: "Quarterly update", textMode: "key_points"}' |
curl -s -X POST "$API/runs" \
-H "Authorization: Bearer $VIDONTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @-)
echo "$RUN"
RUN_ID=$(echo "$RUN" | jq -r '.id // empty')
const text = await readFile("article.txt", "utf8");
const response = await fetch(`${API}/runs`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": randomUUID() },
body: JSON.stringify({ text, title: "Quarterly update", textMode: "key_points" }),
});
let run = await response.json();
if (!response.ok) throw new Error(`${run.error.code}: ${run.error.message}`);
console.log(run.id, run.status);
with open("article.txt", encoding="utf-8") as file:
text = file.read()
response = session.post(
f"{API}/runs",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={"text": text, "title": "Quarterly update", "textMode": "key_points"},
)
run = response.json()
if not response.ok:
raise RuntimeError(f"{run['error']['code']}: {run['error']['message']}")
print(run["id"], run["status"])
Fetch the run with GET /runs/{runId} until status is no longer running. A video usually takes a few minutes. Checking every 10 seconds stays well inside the rate limits. To skip polling altogether, set up a webhook and get told when the run finishes.
# Uses RUN_ID from the previous step. Checks every 10 seconds.
while true; do
RUN=$(curl -s "$API/runs/$RUN_ID" -H "Authorization: Bearer $VIDONTO_API_KEY")
STATUS=$(echo "$RUN" | jq -r .status)
echo "$STATUS $(echo "$RUN" | jq -r .stage) $(echo "$RUN" | jq -r .progress)%"
[ "$STATUS" != "running" ] && break
sleep 10
done
echo "$RUN" | jq '{status, error, video}'
while (run.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 10_000));
const response = await fetch(`${API}/runs/${run.id}`, { headers });
run = await response.json();
if (!response.ok) throw new Error(`${run.error.code}: ${run.error.message}`);
console.log(run.status, run.stage, `${Math.round(run.progress)}%`);
}
if (run.status !== "succeeded") {
throw new Error(run.error ?? `Run ${run.status}`);
}
while run["status"] == "running":
time.sleep(10)
response = session.get(f"{API}/runs/{run['id']}")
response.raise_for_status()
run = response.json()
print(run["status"], run["stage"], f"{round(run['progress'])}%")
if run["status"] != "succeeded":
raise RuntimeError(run["error"] or f"Run {run['status']}")
| status | Meaning | What to do |
|---|
running | Still working. stage and progress (0–100) show how far it got. | Check again later. |
succeeded | Finished. video holds the download path and the length in seconds. | Download the MP4. |
failed | error says why. | If retryable is true, retry it from the step that failed. |
cancelled | Stopped by you, in the API or the web app. | Nothing; unused credits were returned. |
A run passes through four stages in order:
transcript: reads the captions of the video, the text of the web page, or your pasted text.summary: writes the script.storyboard: plans the frames, captions and narration.render: records the voice-over and renders the MP4. When the run succeeds, stage is done.
Once video is set, download the file from GET /runs/{runId}/video. The response is the MP4 itself, sent as an attachment.
curl -fL -o summary.mp4 "$API/runs/$RUN_ID/video" \
-H "Authorization: Bearer $VIDONTO_API_KEY"
// run.video.downloadUrl is a path on this host, e.g. /api/v1/runs/{id}/video
const video = await fetch(new URL(run.video.downloadUrl, API), { headers });
if (!video.ok) throw new Error(`Download failed with status ${video.status}`);
await writeFile("summary.mp4", Buffer.from(await video.arrayBuffer()));
console.log("Saved summary.mp4");
with session.get(f"{API}/runs/{run['id']}/video", stream=True) as video:
video.raise_for_status()
with open("summary.mp4", "wb") as file:
for chunk in video.iter_content(chunk_size=1024 * 1024):
file.write(chunk)
print("Saved summary.mp4")
{
"id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
"status": "succeeded",
"stage": "done",
"progress": 100,
"error": null,
"retryable": false,
"title": "How transformers work",
"source": {
"kind": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
},
"options": {
"targetDuration": 90,
"summaryModel": "gpt-5.6-terra",
"videoFormat": "landscape",
"textMode": "narration",
"captionLength": "short",
"aiImages": false,
"voiceId": "21m00Tcm4TlvDq8ikWAM",
"voiceModel": "eleven_v4",
"transcriptLanguage": "en",
"openingStyle": "coming_up",
"endingStyle": "none",
"customSignOff": null,
"creditSource": false,
"instructions": null
},
"stages": [
{
"id": "transcript",
"label": "Fetching transcript",
"status": "done",
"progress": 100
},
{
"id": "summary",
"label": "Script",
"status": "done",
"progress": 100
},
{
"id": "storyboard",
"label": "Storyboard",
"status": "done",
"progress": 100
},
{
"id": "render",
"label": "Voiceover & render",
"status": "done",
"progress": 100
}
],
"chargedCredits": 48.2,
"video": {
"downloadUrl": "/api/v1/runs/3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11/video",
"durationSeconds": 92.4
},
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:06:40.000Z"
}
Everything also shows up in the app
Runs started with a key appear in your Quick Render list in the web app, and runs started in the app appear in GET /runs. You can watch, cancel or download them in either place.