Developers

Make summary videos from your own code.

The Vidonto API turns a YouTube link, a web page or pasted text into a narrated, captioned MP4. Start a video with one request, follow its progress or get a webhook when it is done, and pay with the credits you already have.

Base URL
https://www.vidonto.com/api/v1
Authentication
Authorization: Bearer qsv_…

Start here

Getting started

The Vidonto API lets your own code do what Quick Render does in the web app: start a summary video from a YouTube link, a web page or pasted text, follow its progress, download the finished MP4 and check your credits. Videos started through the API use your credits and saved settings, and appear in your Quick Render list in the app next to the ones you started by hand.

1. Create an API key

  • Create an account and confirm your email address. Starting videos needs a verified email.
  • Open the API keys page in the app, give the key a name (for example the program that will use it) and select Create key.
  • Copy the key straight away. It looks like qsv_ followed by 48 letters and digits, and it is shown only once. You can have up to 10 active keys.

Keep your key secret

A key acts as your account and spends your credits. Keep it on your server, in an environment variable or a secrets manager. Never put it in browser or mobile app code, and never commit it to a repository. If a key leaks, revoke it on the API keys page; it stops working immediately. Resetting your password also revokes all your keys.

2. Use the base URL

All endpoints live under one base URL. Requests and responses are JSON, apart from the MP4 download.

Base URL
https://www.vidonto.com/api/v1

3. Make your first request

Checking your credit balance is free and a good way to confirm the key works. Every sample on this page reads the key from a VIDONTO_API_KEY environment variable.

export VIDONTO_API_KEY="qsv_..."   # from the API keys page

curl https://www.vidonto.com/api/v1/credits \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
// Node.js 18 or later. Run with VIDONTO_API_KEY set in the environment.
const response = await fetch("https://www.vidonto.com/api/v1/credits", {
  headers: { Authorization: `Bearer ${process.env.VIDONTO_API_KEY}` },
});
console.log(response.status, await response.json()); // 200 { balance: 412.5, exempt: false }
# pip install requests. Run with VIDONTO_API_KEY set in the environment.
import os
import requests

response = requests.get(
    "https://www.vidonto.com/api/v1/credits",
    headers={"Authorization": f"Bearer {os.environ['VIDONTO_API_KEY']}"},
)
print(response.status_code, response.json())  # 200 {'balance': 412.5, 'exempt': False}

Basics

Authentication

Send your key as a bearer token in the Authorization header of every request. The API only accepts keys: signing in to the web app does not give access, and keys never work on the app's own pages.

Header
Authorization: Bearer qsv_YOUR_API_KEY

When the key is missing or not accepted, the API answers with status 401:

SituationStatuserror.code
No Authorization header, or not a Bearer token401missing_api_key
The key is mistyped, was revoked, or belongs to a disabled account401invalid_api_key
401 response
{
  "error": {
    "code": "invalid_api_key",
    "message": "This API key is invalid or has been revoked."
  }
}

Revoking a key on the API keys page takes effect on the next request. Programs that still use it get invalid_api_key, so create the replacement key and deploy it before you revoke the old one.

Ten minutes

Quickstart: from link to MP4

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.

0. Set up

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).

202 response
{
  "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"
}

…or from pasted text

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"])

2. Wait for it to finish

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']}")
statusMeaningWhat to do
runningStill working. stage and progress (0–100) show how far it got.Check again later.
succeededFinished. video holds the download path and the length in seconds.Download the MP4.
failederror says why.If retryable is true, retry it from the step that failed.
cancelledStopped 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.

3. Download the MP4

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")
The finished run
{
  "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.

Customize

Video options

Every option is optional, on both POST /runs and POST /runs/batch. For each one you leave out, the API uses the first of:

  1. the value in your request,
  2. your saved setting from the Settings page in the app, for the options marked Saved settings below,
  3. the server default.

GET /options returns the allowed values and, under defaults, exactly what a run gets when you send nothing. Every run also reports the options it actually used in its options field. Unknown fields are rejected with validation_error, so a typo never silently falls back to a default.

OptionValuesWhen omitted
targetDurationSeconds, 10 or more.90. A target, not a limit: the narration always plays in full.
videoFormatlandscape (16:9), vertical (9:16), square (1:1), portrait (4:5)landscape
textModenarration (a spoken script) or key_points (the main points on screen)narration
captionLengthshort (a few words at a time), medium (about one line), sentence (a whole sentence when it fits)The text mode’s default: short for narration, medium for key points.
summaryModelA script model id from GET /options.Saved settings then the recommended model.
voiceIdA voice id from GET /options.Saved settings then the first available voice.
voiceModeleleven_v4 or eleven_multilingual_v2Saved settings then eleven_v4.
transcriptLanguagePreferred YouTube caption language, such as en or pt-BR.Saved settings then en.
openingStyleSee the table below.Saved settings then coming_up.
endingStyleSee the table below.Saved settings then none.
customSignOffYour closing line, up to 200 characters and 20 words.Saved settings
aiImagestrue or false. Generates images for the frames and costs more credits.false
creditSourcetrue or false. Credits the original source in the video.false
instructionsFree text, up to 1,000 characters. Steers how the script and storyboard are written. See Instructions.None.

Openings and endings

A short opening and ending can frame the summary. Pick them with openingStyle and endingStyle:

openingStyleWhat it does
noneStarts straight into the first point.
coming_upA quick preview that lists the points the video covers.
cliffhangerTeases the most surprising point without giving it away.
questionOpens with the central question the video answers.
bottom_lineStates the biggest takeaway up front, then explains it.
endingStyleWhat it does
noneEnds on the last point.
recapA quick recap of the main points in order.
takeawayA one-sentence bottom line to remember.
reflectionCloses with a question for the viewer to think about.
originalSends the viewer to the full video or article.
customEnds with your own line from customSignOff, spoken exactly as written.

With endingStyle custom, the run needs a sign-off: send customSignOff or save one in Settings. A missing or too-long sign-off is rejected with invalid_sign_off.

Request body
{
  "url": "https://example.com/article",
  "openingStyle": "question",
  "endingStyle": "custom",
  "customSignOff": "Follow for a new summary every Monday."
}

Instructions

Use instructions to steer how the video is written: what to focus on, the tone, who it is for, or what to emphasise. For example “focus on the pricing section”, “casual tone for beginners” or “emphasise the risks”. The script follows them, and the storyboard uses them for emphasis and headline wording.

Instructions are guidance, not a new prompt. They can’t change the video’s length, its format, or the rule that the script only says what the source supports, so asks like “ignore the word limit” or “make up statistics” are ignored. Instructions are trimmed, and longer than 1,000 characters is rejected with validation_error. They cost nothing extra, and every run echoes them back in options.instructions (null when none were given), including in webhooks.

Request body
{
  "url": "https://example.com/article",
  "targetDuration": 90,
  "instructions": "Focus on the pricing section. Casual tone for beginners."
}

Sources

SourceSendNotes
YouTube videourlUses the video’s captions, in transcriptLanguage when available. Videos without captions fail at the first step.
Web pageurlUses the main text of the page. Pages behind a sign-in or paywall can’t be read.
Pasted texttext, optional titleEmpty text is rejected with empty_text. With creditSource, pasted text needs a title (source_credit_incomplete).

Long sources

Sources have a maximum length, given in characters in the error message. Pasted text over the limit is refused when you start the run, with source_too_long. A link whose transcript or page turns out to be too long fails before the script is written, and the run’s error says so.

Try your defaults now: curl https://www.vidonto.com/api/v1/options -H "Authorization: Bearer $VIDONTO_API_KEY", or see the GET /options reference.

Billing

Credits

Videos are paid for with credits from your account balance, the same balance the web app uses. Reading endpoints (runs, options, estimates, credits, webhooks) are free. Buy more on the Credits page in the app.

Held when a run starts, settled when it finishes

  • Start. The run’s estimated credits are held: they leave your spendable balance straight away, so two runs can’t both count on the same credits.
  • Each step. As each paid step finishes, it is charged for what it actually used. The charge comes out of the hold first. chargedCredits on the run shows the total so far.
  • Finish. When the run succeeds, fails or is cancelled, whatever is left of the hold is returned to your balance. Steps that never ran are never charged.
  • Retry. Retrying a failed run holds credits again for the steps still to run. Steps that already finished are not repeated or charged twice.

Estimate before you start

GET /estimate uses the same options and saved settings as a real run and tells you whether your balance covers it. Set count for a batch.

Estimate a batch of five vertical videos
curl "https://www.vidonto.com/api/v1/estimate?sourceKind=mixed&count=5&targetDuration=60" \
  -H "Authorization: Bearer $VIDONTO_API_KEY"

The estimate for links assumes a typical source length (assumedSourceMinutes). Longer sources and AI images cost more; the run is always charged for what it really used. See the GET /estimate reference.

Not enough credits: 402

If your balance does not cover the hold, the run does not start and nothing is charged. A batch starts all of its runs or none of them. The response says how many credits you have and how many were needed:

402 response
{
  "error": {
    "code": "insufficient_credits",
    "message": "This needs about 48 credits and your balance is 12.5. Get more credits to continue.",
    "balance": 12.5,
    "required": 48
  }
}

Balance and history

GET /credits returns your spendable balance. GET /credits/history lists every change, newest first, in pages (pass nextBefore back as before). Each entry’s credits is the change to your balance and balanceAfter the balance after it.

kindMeaning
reserveCredits held when a run started (negative).
chargeA finished step. charged is the step’s full price and step names it; credits is only the part the hold did not cover.
releaseThe unused part of a hold, returned when the run finished (positive).
grantCredits added to your account, such as starter credits.
purchaseCredits you bought.
refundCredits removed because a purchase was refunded.
adjustmentA manual correction.

Accounts that are not charged

Some accounts are not charged for videos. For them exempt is true on the balance, the estimate and the history entries, and holds and charges deduct nothing.

Reliability

Idempotency

Network errors happen. If a request to start a video times out, you can’t tell whether the run started. Send an Idempotency-Key header on POST /runs and POST /runs/batch and repeat the request with the same key: the API starts the run at most once.

Header
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
  • Use a new random value, such as a UUID, for each video you mean to start. Up to 255 printable characters, no spaces.
  • Keys are remembered for 24 hours per account. Repeating the same request returns the run that was already started, as it is now, with 202 and the header Idempotent-Replayed: true. No new credits are held.
  • The same key with a different body or endpoint returns 422 idempotency_key_reused. While the first request is still being handled, a repeat returns 409 idempotency_in_progress; wait a moment and send it again.
  • If the first request was refused (for example with 402 or a validation error), no run exists and the key is freed, so you can fix the problem and retry with the same key.

Reliability

Errors

Errors use standard HTTP status codes and always have the same JSON shape. Branch on error.code, which never changes; error.message is written for people and may change. Some errors add details next to the code.

422 response
{
  "error": {
    "code": "validation_error",
    "message": "targetDuration: Number must be greater than or equal to 10",
    "issues": [
      {
        "path": "targetDuration",
        "message": "Number must be greater than or equal to 10"
      }
    ]
  }
}
Statuserror.codeMeaning
400invalid_jsonThe request body is not valid JSON.
401missing_api_keyNo Authorization header, or it is not a Bearer token.
401invalid_api_keyThe key is wrong, was revoked, or its account is disabled.
402insufficient_creditsYour balance doesn’t cover the run. Includes balance and required.
403email_unverifiedConfirm your email address in the app before starting videos or adding webhooks.
404not_foundNo such run, webhook, delivery or endpoint, or it belongs to another account.
409video_not_readyThe run has no finished video yet. Includes the run’s status and stage.
409retry_not_safeThe run is not a failed run with retryable set, so it can’t be retried.
409conflictThe run is in the wrong state for this action, such as cancelling a finished run.
409idempotency_in_progressA request with the same Idempotency-Key is still being handled. Try again shortly.
409webhook_limitThe account already has 10 webhook endpoints.
409webhook_busyAnother endpoint is being added at the same moment. Try again.
409delivery_busyThat delivery is being sent right now. Try again.
413payload_too_largeThe request body is too large.
422validation_errorA field is missing, has the wrong type or value, or is unknown. issues lists each problem with its path.
422invalid_sourceBoth url and text were sent.
422invalid_source_urlThe link is missing or is not a YouTube video or web page link.
422empty_textThe pasted text is empty.
422unknown_voiceThe voiceId is not in GET /options.
422invalid_sign_offThe custom ending’s sign-off is missing or too long.
422source_credit_incompleteCrediting pasted text needs a title.
422source_too_longThe pasted text is longer than the allowed source length.
422invalid_idempotency_keyThe Idempotency-Key is empty, too long or contains spaces.
422idempotency_key_reusedThe Idempotency-Key was already used with a different request.
422invalid_webhook_urlThe webhook URL is not a public HTTPS address.
429rate_limitedToo many requests. Wait retryAfterSeconds (also in the Retry-After header).
500internal_errorSomething went wrong on our side. Try again.
503…_unavailableA service the run needs is temporarily unavailable, for example rendering_unavailable or narration_unavailable. Try again later.

A run that fails after it started is not an HTTP error: the run’s status becomes failed and its error explains why. See Wait for it to finish.

Reliability

Rate limits

Limits are counted per key and per account (all your keys together) in fixed one-minute windows. Going over returns 429 rate_limited with a Retry-After header and retryAfterSeconds in the body.

GroupEndpointsPer keyPer account
ReadingGetting and listing runs, cancelling, downloading videos, /options, /estimate, /credits, listing webhooks and deliveries120 / minute240 / minute
Starting videosPOST /runs, POST /runs/batch (one request however many links), POST /runs/{runId}/retry10 / minute20 / minute
Webhook changesAdding, changing, deleting, rotating, test events and resends30 / minute60 / minute
  • When you get a 429, wait the number of seconds given before trying again.
  • Poll a run every 5–10 seconds at most, or use webhooks instead of polling.
  • To start many videos, use POST /runs/batch with up to 100 links.
  • The OpenAPI document is not rate limited and needs no key.

Notifications

Webhooks

Instead of polling, give the API an HTTPS address and it will send a signed POST request there whenever one of your runs finishes, including runs started in the web app. Add endpoints on the API keys page or with POST /webhooks; you can have up to 10.

Add an endpoint

Add an endpoint
curl -X POST https://www.vidonto.com/api/v1/webhooks \
  -H "Authorization: Bearer $VIDONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/vidonto", "events": ["run.succeeded", "run.failed"]}'

The response includes the endpoint’s signing secret (it starts with whsec_). It is shown only once: store it with your other secrets. Then send a test event with POST /webhooks/{webhookId}/test to check your handler.

  • The URL must be a public HTTPS address. Private and local network addresses are refused.
  • Your endpoint must answer with a 2xx status within 10 seconds. Redirects are not followed and count as failures.
  • Answer first and do slow work afterwards, for example by putting the event on a queue.

Events

EventSent when
run.succeededA run finished and its video is ready to download.
run.failedA run failed. data.run.error says why and data.run.retryable whether it can be retried.
run.cancelledA run was cancelled, in the API or the web app.
webhook.testYou asked for a test event. Its run is a sample, not one of yours.

A run that fails and is then retried can finish again, so it can send run.failed and later run.succeeded, each as a separate event with its own ID. Events are only recorded for endpoints that are turned on and subscribed at the moment the run finishes.

Payload

The body is a JSON event. data.run is the same run object that GET /runs/{runId} returns.

Request headers
POST /hooks/vidonto HTTP/1.1
Content-Type: application/json
User-Agent: QuickSum-Webhooks/1.0
QuickSum-Event-Id: evt_6c1f0e9a2b7d4c3e8f5a1b2c3d4e5f60
QuickSum-Event-Type: run.succeeded
QuickSum-Timestamp: 1790856400
QuickSum-Signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

The header names start with QuickSum-, the product’s former name. They stay the same so existing integrations keep working.

Request body
{
  "id": "evt_6c1f0e9a2b7d4c3e8f5a1b2c3d4e5f60",
  "type": "run.succeeded",
  "createdAt": "2026-10-01T12:06:40.000Z",
  "data": {
    "run": {
      "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"
    }
  }
}

Verify the signature

Every request is signed with your endpoint’s secret, so you can be sure it came from Vidonto and was not changed. To check it:

  1. Read the QuickSum-Timestamp header (Unix seconds) and reject the request if it is more than 300 seconds from your clock. This stops old requests from being replayed.
  2. Compute an HMAC-SHA256 of <timestamp>.<raw request body>, using the whole secret, including whsec_, as the key. Hex-encode the result.
  3. Compare it, in constant time, with each v1= value in the QuickSum-Signature header. The header can hold several values separated by spaces; accept the request if any one matches.

Use the raw body

Compute the signature over the exact bytes you received. Parsing the JSON and serializing it again changes spacing and key order, and the signature will not match.
// Express example. Verify against the raw body, before any JSON parsing.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.VIDONTO_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

function isValidSignature(rawBody, timestamp, signatureHeader) {
  if (!timestamp || !signatureHeader) return false;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  // The header can hold several space-separated "v1=" values.
  return signatureHeader.split(" ").some((part) => {
    if (!part.startsWith("v1=")) return false;
    const given = Buffer.from(part.slice(3), "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

const handled = new Set(); // use a database table in production

const app = express();
app.post("/hooks/vidonto", express.raw({ type: "application/json" }), (req, res) => {
  if (!isValidSignature(req.body, req.get("QuickSum-Timestamp"), req.get("QuickSum-Signature"))) {
    return res.status(400).send("Invalid signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200); // answer within 10 seconds, then do the work

  if (handled.has(event.id)) return; // a retry of an event you already handled
  handled.add(event.id);
  if (event.type === "run.succeeded") {
    console.log("Video ready:", event.data.run.id, event.data.run.video.downloadUrl);
  }
});
app.listen(3000);
# Flask example. Verify against the raw body, before any JSON parsing.
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

SECRET = os.environ["VIDONTO_WEBHOOK_SECRET"].encode()  # whsec_...
TOLERANCE_SECONDS = 300
handled = set()  # use a database table in production


def is_valid_signature(raw_body, timestamp, signature_header):
    if not timestamp or not signature_header:
        return False
    try:
        if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
            return False
    except ValueError:
        return False
    expected = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    # The header can hold several space-separated "v1=" values.
    return any(
        part.startswith("v1=") and hmac.compare_digest(part[3:], expected)
        for part in signature_header.split(" ")
    )


app = Flask(__name__)


@app.post("/hooks/vidonto")
def vidonto_webhook():
    raw_body = request.get_data()
    if not is_valid_signature(
        raw_body,
        request.headers.get("QuickSum-Timestamp"),
        request.headers.get("QuickSum-Signature"),
    ):
        abort(400)
    event = json.loads(raw_body)
    if event["id"] not in handled:  # skip retries of events you already handled
        handled.add(event["id"])
        if event["type"] == "run.succeeded":
            print("Video ready:", event["data"]["run"]["id"])
    return "", 200

Retries

If your endpoint doesn’t answer with a 2xx status in time, the delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 4 hours, 8 hours and 10 hours: 8 attempts over about a day. Every attempt has a fresh timestamp and signature but the same event ID and body. After the last attempt the delivery is marked failed.

  • When 5 events in a row run out of attempts, the endpoint is turned off and its disabledReason becomes failing. Events that happen while it is off are not sent later. Turn it back on in the app or with PATCH /webhooks/{webhookId} and {"enabled": true}.
  • See each endpoint’s recent deliveries, with status codes and errors, with GET /webhooks/{webhookId}/deliveries, and send one again with resend.
  • If you lose the secret or think it leaked, rotate it. The old secret stops working at once.

Handle each event once

Because of retries and resends, the same event can arrive more than once, and events can arrive out of order. Store the event id (also sent as the QuickSum-Event-Id header) when you handle an event, and skip any event whose ID you have already seen. If order matters, fetch the run with GET /runs/{runId} for its current state.

Reference

Endpoint reference

Every path below is relative to https://www.vidonto.com/api/v1. The machine-readable version of this reference is the OpenAPI document, at openapi.json and openapi.yaml. Responses shorten long nested objects to { … }; the full run object is shown in the quickstart.

Runs

Start, follow, cancel and retry videos, and download the finished MP4.

Start a video from a link or pasted text

POST/runs

Send either url or text. The run’s estimated credits are held when it starts; you are charged for what each step actually uses and the rest is returned. Needs a verified email address.

ParameterInTypeDescription
Idempotency-KeyheaderstringUp to 255 printable characters without spaces, such as a UUID. See Idempotency.
urlbodystringA YouTube video or web page link, up to 2,000 characters.
textbodystringPasted text to summarize, instead of url.
titlebodystringA title for pasted text, up to 300 characters.
targetDuration, videoFormat, …bodyoptionsAny of the video options. All are optional; omitted ones use your saved settings. See Options.
Example request
curl -X POST https://www.vidonto.com/api/v1/runs \
  -H "Authorization: Bearer $VIDONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "targetDuration": 60, "videoFormat": "vertical"}'
Response · 202 Accepted
{
  "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"
}

Errors: insufficient_credits, email_unverified, validation_error, invalid_source, invalid_source_url, empty_text, unknown_voice, source_too_long, idempotency_key_reused, idempotency_in_progress and the common errors.

Start one video per link

POST/runs/batch

Up to 100 links, all with the same options. Every link is checked before anything starts, and credits for all runs are held up front: if one link is invalid or your balance does not cover them all, nothing starts. Counts as one request for rate limits. Needs a verified email address.

ParameterInTypeDescription
Idempotency-KeyheaderstringUp to 255 printable characters without spaces, such as a UUID. See Idempotency.
urlsrequiredbodystring[]1 to 100 YouTube or web page links.
targetDuration, videoFormat, …bodyoptionsAny of the video options. All are optional; omitted ones use your saved settings. See Options.
Example request
curl -X POST https://www.vidonto.com/api/v1/runs/batch \
  -H "Authorization: Bearer $VIDONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://example.com/article"], "targetDuration": 90}'
Response · 202 Accepted
{
  "runs": [
    {
      "id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
      "status": "running",
      "stage": "transcript",
      "progress": 0,
      "error": null,
      "retryable": false,
      "title": null,
      "source": { … },
      "options": { … },
      "stages": [ … ],
      "chargedCredits": 0,
      "video": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "updatedAt": "2026-10-01T12:00:00.000Z"
    },
    {
      "id": "5d2a8f14-3c7b-4e1a-9f60-8b2c4d6e0a13",
      "status": "running",
      "stage": "transcript",
      "progress": 0,
      "error": null,
      "retryable": false,
      "title": null,
      "source": { … },
      "options": { … },
      "stages": [ … ],
      "chargedCredits": 0,
      "video": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "updatedAt": "2026-10-01T12:00:00.000Z"
    }
  ]
}

Errors: insufficient_credits, email_unverified, validation_error, invalid_source_url, idempotency_key_reused, idempotency_in_progress and the common errors.

List your runs, newest first

GET/runs

Includes runs started in the web app.

ParameterInTypeDescription
limitqueryinteger1 to 100. Default 20.
Example request
curl "https://www.vidonto.com/api/v1/runs?limit=20" \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "runs": [
    {
      "id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
      "status": "running",
      "stage": "render",
      "progress": 78,
      "error": null,
      "retryable": false,
      "title": "How transformers work",
      "source": { … },
      "options": { … },
      "stages": [ … ],
      "chargedCredits": 21.3,
      "video": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "updatedAt": "2026-10-01T12:03:10.000Z"
    }
  ]
}

Get one run’s status and progress

GET/runs/{runId}

Poll this until status is no longer running, or use a webhook.

ParameterInTypeDescription
runIdrequiredpathuuidThe run’s id.
Example request
curl https://www.vidonto.com/api/v1/runs/3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11 \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
  "status": "running",
  "stage": "render",
  "progress": 78,
  "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": "running",
      "progress": 12
    }
  ],
  "chargedCredits": 21.3,
  "video": null,
  "createdAt": "2026-10-01T12:00:00.000Z",
  "updatedAt": "2026-10-01T12:03:10.000Z"
}

Errors: not_found and the common errors.

Cancel a run

POST/runs/{runId}/cancel

Works on running runs, and on failed runs you no longer want to retry. Credits held for steps that have not run yet are returned. Cancelling a run that already succeeded or was cancelled returns 409.

ParameterInTypeDescription
runIdrequiredpathuuidThe run’s id.
Example request
curl -X POST https://www.vidonto.com/api/v1/runs/3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11/cancel \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
  "status": "cancelled",
  "stage": "render",
  "progress": 78,
  "error": "Cancelled by user.",
  "retryable": false,
  "title": "How transformers work",
  "source": { … },
  "options": { … },
  "stages": [ … ],
  "chargedCredits": 21.3,
  "video": null,
  "createdAt": "2026-10-01T12:00:00.000Z",
  "updatedAt": "2026-10-01T12:03:10.000Z"
}

Errors: not_found, conflict and the common errors.

Retry a failed run from the step that failed

POST/runs/{runId}/retry

Only runs with retryable set to true. Steps that already finished are not repeated or charged again; credits are held again for the steps still to run. Needs a verified email address.

ParameterInTypeDescription
runIdrequiredpathuuidThe run’s id.
Example request
curl -X POST https://www.vidonto.com/api/v1/runs/3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11/retry \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 202 Accepted
{
  "id": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
  "status": "running",
  "stage": "render",
  "progress": 78,
  "error": null,
  "retryable": false,
  "title": "How transformers work",
  "source": { … },
  "options": { … },
  "stages": [ … ],
  "chargedCredits": 21.3,
  "video": null,
  "createdAt": "2026-10-01T12:00:00.000Z",
  "updatedAt": "2026-10-01T12:03:10.000Z"
}

Errors: not_found, retry_not_safe, conflict, insufficient_credits, email_unverified and the common errors.

Download the finished MP4

GET/runs/{runId}/video

Available once the run’s video field is set. The response is the MP4 itself (video/mp4), sent as an attachment unless inline=true. Supports HEAD and single Range requests such as Range: bytes=0-1048575, which answer 206 Partial Content; a range past the end of the file answers 416.

ParameterInTypeDescription
runIdrequiredpathuuidThe run’s id.
inlinequery"true" | "false"Send true to play the video in a browser instead of downloading it.
RangeheaderstringA single byte range, to resume or stream the download.
Example request
curl -fL -o summary.mp4 https://www.vidonto.com/api/v1/runs/3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11/video \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
(the MP4 file, with Content-Type: video/mp4, Content-Length and Accept-Ranges: bytes)

Errors: not_found, video_not_ready and the common errors.

Options and estimates

The values each option accepts, your effective defaults, and credit estimates.

Allowed video options and your effective defaults

GET/options

defaults is exactly what a run gets when you send no options: your saved settings, filled in with the server defaults. Lists of voices and script models can change, so read them from here rather than hard-coding them. availability is false while script writing or narration is temporarily unavailable.

Example request
curl https://www.vidonto.com/api/v1/options \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "summaryModels": [
    { "id": "gpt-5.6-terra", "name": "…", "relativeCost": "moderate", "typicalCredits": 6.2 },
    …
  ],
  "videoFormats": [
    { "id": "landscape", "label": "YouTube", "purpose": "Regular videos", "aspectRatio": "16:9", "width": 1280, "height": 720 },
    { "id": "vertical", "label": "Shorts & TikTok", "purpose": "Vertical social video", "aspectRatio": "9:16", "width": 720, "height": 1280 },
    { "id": "square", "label": "Square", "purpose": "Social feeds", "aspectRatio": "1:1", "width": 1080, "height": 1080 },
    { "id": "portrait", "label": "Portrait", "purpose": "Instagram & social", "aspectRatio": "4:5", "width": 1080, "height": 1350 }
  ],
  "targetDuration": { "minSeconds": 10, "defaultSeconds": 90 },
  "textModes": ["narration", "key_points"],
  "captionLengths": [
    { "id": "short", "label": "Short", "description": "A few words at a time" },
    { "id": "medium", "label": "Medium", "description": "About one line at a time" },
    { "id": "sentence", "label": "Full sentence", "description": "A whole sentence at a time when it fits" }
  ],
  "voices": [{ "id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel" }, …],
  "voiceModels": [{ "id": "eleven_v4", "name": "…" }, { "id": "eleven_multilingual_v2", "name": "…" }],
  "transcriptLanguages": [{ "code": "en", "name": "English" }, …],
  "openingStyles": ["none", "coming_up", "cliffhanger", "question", "bottom_line"],
  "endingStyles": ["none", "recap", "takeaway", "reflection", "original", "custom"],
  "customSignOffMaxCharacters": 200,
  "aiImagesAvailable": true,
  "defaults": {
    "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
  },
  "availability": { "summary": true, "narration": true }
}

Estimate the credits for a video

GET/estimate

Omitted options use your saved settings, as when starting a run. Links are estimated for a typical source length (assumedSourceMinutes); the real charge depends on the source and what each step uses. enoughCredits tells you whether your balance covers totalCredits.

ParameterInTypeDescription
sourceKindqueryyoutube | webpage | text | mixedDefault youtube. Use mixed for a batch of YouTube and web page links.
countqueryintegerNumber of videos, 1 to 100. Default 1.
targetDurationqueryintegerTarget length in seconds, at least 10.
summaryModelquerystringA script model id from GET /options.
aiImagesquery"true" | "false"Include AI images.
voiceModelquerystringA voice model id from GET /options.
Example request
curl "https://www.vidonto.com/api/v1/estimate?sourceKind=youtube&count=1&targetDuration=90" \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "perRunCredits": 38.4,
  "count": 1,
  "totalCredits": 38.4,
  "assumedSourceMinutes": 10,
  "disclosure": "Estimated credits per video…",
  "balance": 412.5,
  "exempt": false,
  "enoughCredits": true,
  "options": {
    "sourceKind": "youtube",
    "targetDuration": 90,
    "summaryModel": "gpt-5.6-terra",
    "aiImages": false,
    "voiceModel": "eleven_v4"
  }
}

Errors: validation_error and the common errors.

Credits

Your balance and every change to it.

Your credit balance

GET/credits

balance is what you can spend now; credits held by running videos are already taken out. It can be below zero after a refund.

Example request
curl https://www.vidonto.com/api/v1/credits \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "balance": 412.5,
  "exempt": false
}

Your credit history, newest first

GET/credits/history

One entry per change to your balance. To page back, pass the previous response’s nextBefore as before; it is null on the last page. See Credits for the entry kinds.

ParameterInTypeDescription
limitqueryinteger1 to 500. Default 100.
beforequeryISO 8601 timeOnly entries older than this.
Example request
curl "https://www.vidonto.com/api/v1/credits/history?limit=50" \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "entries": [
    {
      "id": "b71c2f0e-4d3a-4f8e-9a1b-6c5d4e3f2a10",
      "kind": "charge",
      "credits": 0,
      "charged": 9.8,
      "balanceAfter": 374.1,
      "exempt": false,
      "step": "summary",
      "note": null,
      "runId": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
      "jobId": null,
      "jobType": null,
      "title": "How transformers work",
      "createdAt": "2026-10-01T12:01:05.000Z"
    },
    {
      "id": "0a9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c",
      "kind": "reserve",
      "credits": -38.4,
      "charged": 0,
      "balanceAfter": 374.1,
      "exempt": false,
      "step": null,
      "note": null,
      "runId": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
      "jobId": null,
      "jobType": null,
      "title": "How transformers work",
      "createdAt": "2026-10-01T12:00:00.000Z"
    }
  ],
  "nextBefore": "2026-10-01T12:00:00.000Z"
}

Errors: validation_error and the common errors.

Webhooks

Manage the endpoints that are told when your runs finish. You can also do all of this on the API keys page in the app. See Webhooks for how deliveries work.

List your webhook endpoints

GET/webhooks

All endpoints, newest first, with the account limit and the event types you can subscribe to.

Example request
curl https://www.vidonto.com/api/v1/webhooks \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "endpoints": [
    {
      "id": "9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d",
      "url": "https://example.com/hooks/vidonto",
      "events": [
        "run.succeeded",
        "run.failed"
      ],
      "enabled": true,
      "disabledReason": null,
      "disabledAt": null,
      "secretPrefix": "whsec_AbC123",
      "failedEventsInARow": 0,
      "lastSuccessAt": null,
      "lastFailureAt": null,
      "createdAt": "2026-10-01T11:00:00.000Z",
      "updatedAt": "2026-10-01T11:00:00.000Z"
    }
  ],
  "maxEndpoints": 10,
  "eventTypes": [
    "run.succeeded",
    "run.failed",
    "run.cancelled"
  ]
}

Add a webhook endpoint

POST/webhooks

Returns the signing secret once. Store it straight away; it can’t be read again (rotate it if it is lost). Up to 10 endpoints per account. Needs a verified email address.

ParameterInTypeDescription
urlrequiredbodystringA public HTTPS address, up to 2,048 characters.
eventsbodystring[]Any of run.succeeded, run.failed, run.cancelled. Default: all three.
enabledbodybooleanDefault true.
Example request
curl -X POST https://www.vidonto.com/api/v1/webhooks \
  -H "Authorization: Bearer $VIDONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/vidonto", "events": ["run.succeeded", "run.failed"]}'
Response · 201 Created
{
  "endpoint": {
    "id": "9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d",
    "url": "https://example.com/hooks/vidonto",
    "events": [
      "run.succeeded",
      "run.failed"
    ],
    "enabled": true,
    "disabledReason": null,
    "disabledAt": null,
    "secretPrefix": "whsec_AbC123",
    "failedEventsInARow": 0,
    "lastSuccessAt": null,
    "lastFailureAt": null,
    "createdAt": "2026-10-01T11:00:00.000Z",
    "updatedAt": "2026-10-01T11:00:00.000Z"
  },
  "secret": "whsec_AbC123…"
}

Errors: invalid_webhook_url, validation_error, webhook_limit, webhook_busy, email_unverified and the common errors.

Get one webhook endpoint

GET/webhooks/{webhookId}

failedEventsInARow counts events whose deliveries ran out of retries. disabledReason is failing when the endpoint was turned off automatically, manual when you turned it off.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
Example request
curl https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "id": "9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d",
  "url": "https://example.com/hooks/vidonto",
  "events": [
    "run.succeeded",
    "run.failed"
  ],
  "enabled": true,
  "disabledReason": null,
  "disabledAt": null,
  "secretPrefix": "whsec_AbC123",
  "failedEventsInARow": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-10-01T11:00:00.000Z",
  "updatedAt": "2026-10-01T11:00:00.000Z"
}

Errors: not_found and the common errors.

Change an endpoint’s URL or events, or turn it on or off

PATCH/webhooks/{webhookId}

Send at least one field. Turning an endpoint back on, or changing its URL, resets its count of failed events.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
urlbodystringA new public HTTPS address.
eventsbodystring[]The event types to receive.
enabledbodybooleanTurn the endpoint on or off.
Example request
curl -X PATCH https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d \
  -H "Authorization: Bearer $VIDONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'
Response · 200 OK
{
  "id": "9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d",
  "url": "https://example.com/hooks/vidonto",
  "events": [
    "run.succeeded",
    "run.failed"
  ],
  "enabled": true,
  "disabledReason": null,
  "disabledAt": null,
  "secretPrefix": "whsec_AbC123",
  "failedEventsInARow": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-10-01T11:00:00.000Z",
  "updatedAt": "2026-10-01T11:00:00.000Z"
}

Errors: not_found, invalid_webhook_url, validation_error and the common errors.

Delete an endpoint and its delivery history

DELETE/webhooks/{webhookId}

Deliveries still waiting to be retried are dropped.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
Example request
curl -X DELETE https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 204 No Content
(no body)

Errors: not_found and the common errors.

Replace the signing secret

POST/webhooks/{webhookId}/rotate-secret

The old secret stops working immediately. Deliveries still waiting to be retried are signed with the new one.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
Example request
curl -X POST https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d/rotate-secret \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "endpoint": {
    "id": "9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d",
    "url": "https://example.com/hooks/vidonto",
    "events": [
      "run.succeeded",
      "run.failed"
    ],
    "enabled": true,
    "disabledReason": null,
    "disabledAt": null,
    "secretPrefix": "whsec_Zx9Qw2",
    "failedEventsInARow": 0,
    "lastSuccessAt": null,
    "lastFailureAt": null,
    "createdAt": "2026-10-01T11:00:00.000Z",
    "updatedAt": "2026-10-01T11:00:00.000Z"
  },
  "secret": "whsec_Zx9Qw2…"
}

Errors: not_found and the common errors.

Send a test event now

POST/webhooks/{webhookId}/test

Sends a webhook.test event with a sample finished run and waits for your endpoint’s answer. Works while the endpoint is turned off. Test events are never retried and don’t count toward turning the endpoint off.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
Example request
curl -X POST https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d/test \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "id": "c4e8a1f2-7b3d-4e9a-a6c5-1d2e3f4a5b6c",
  "eventId": "evt_0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
  "eventType": "webhook.test",
  "runId": null,
  "status": "delivered",
  "attempts": 1,
  "lastStatusCode": 200,
  "lastError": null,
  "lastAttemptAt": "2026-10-01T12:06:41.000Z",
  "nextAttemptAt": null,
  "deliveredAt": "2026-10-01T12:06:41.000Z",
  "createdAt": "2026-10-01T12:06:40.000Z"
}

Errors: not_found and the common errors.

Recent deliveries to an endpoint

GET/webhooks/{webhookId}/deliveries

Newest first. status is pending (will be tried again at nextAttemptAt), delivered or failed (ran out of retries). Finished deliveries are kept for 30 days.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
limitqueryinteger1 to 100. Default 20.
Example request
curl "https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d/deliveries?limit=20" \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "deliveries": [
    {
      "id": "c4e8a1f2-7b3d-4e9a-a6c5-1d2e3f4a5b6c",
      "eventId": "evt_6c1f0e9a2b7d4c3e8f5a1b2c3d4e5f60",
      "eventType": "run.succeeded",
      "runId": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
      "status": "delivered",
      "attempts": 1,
      "lastStatusCode": 200,
      "lastError": null,
      "lastAttemptAt": "2026-10-01T12:06:41.000Z",
      "nextAttemptAt": null,
      "deliveredAt": "2026-10-01T12:06:41.000Z",
      "createdAt": "2026-10-01T12:06:40.000Z"
    }
  ]
}

Errors: not_found, validation_error and the common errors.

Send one delivery again now

POST/webhooks/{webhookId}/deliveries/{deliveryId}/resend

Same event ID and body, with a fresh timestamp and signature. Waits for your endpoint’s answer.

ParameterInTypeDescription
webhookIdrequiredpathuuidThe endpoint’s id.
deliveryIdrequiredpathuuidThe delivery’s id.
Example request
curl -X POST https://www.vidonto.com/api/v1/webhooks/9b2d7c41-0f3e-4a6b-8c5d-2e1f0a9b8c7d/deliveries/c4e8a1f2-7b3d-4e9a-a6c5-1d2e3f4a5b6c/resend \
  -H "Authorization: Bearer $VIDONTO_API_KEY"
Response · 200 OK
{
  "id": "c4e8a1f2-7b3d-4e9a-a6c5-1d2e3f4a5b6c",
  "eventId": "evt_6c1f0e9a2b7d4c3e8f5a1b2c3d4e5f60",
  "eventType": "run.succeeded",
  "runId": "3f0c6b6e-6a1e-4b8e-9d55-7f3f0a7a2c11",
  "status": "delivered",
  "attempts": 2,
  "lastStatusCode": 200,
  "lastError": null,
  "lastAttemptAt": "2026-10-01T12:06:41.000Z",
  "nextAttemptAt": null,
  "deliveredAt": "2026-10-01T12:06:41.000Z",
  "createdAt": "2026-10-01T12:06:40.000Z"
}

Errors: not_found, delivery_busy and the common errors.

API description

The OpenAPI 3.1 document for this API. It needs no key.

This API description as JSON

GET/openapi.json

Import it into an API client or a code generator.

Example request
curl https://www.vidonto.com/api/v1/openapi.json
Response · 200 OK
{ "openapi": "3.1.0", "info": { "title": "Vidonto API", … }, … }

This API description as YAML

GET/openapi.yaml

The same document as YAML.

Example request
curl https://www.vidonto.com/api/v1/openapi.yaml
Response · 200 OK
openapi: 3.1.0
info:
  title: Vidonto API
  …