← Home

Developers

API Reference

Make promo videos from a script or an AI agent. Public API v1 and the MCP server.

Overview

Start a promo video from a product's landing page URL, follow it while it is made, and download the result. Every video is an ordinary Demo Editor video in the token's project: it shows up in History and opens on the web at its webUrl, where your team can watch it being made, chat with the Director and step in on the storyboard.

The API speaks JSON over HTTPS and is scoped to the one project that owns the token you send. Videos are spent from your organization's video packs. The MCP server sits on top of the same endpoints and tokens, so an AI agent can do all of this too.

Base URL:

https://demoeditor.com/api/v1

A video takes 10 to 30 minutes. The call that starts it answers in about a second; then you poll it.

Authentication

Create a token in Demo Editor under Settings → API. It belongs to one project and acts as the member who created it, so it stops working if that member loses access to the project. The token is shown once (it looks like de_…); only its hash is stored. Revoke it from the same place.

A token has one of two scopes and expires after 30 days, 90 days, 1 year or never, as you choose:

ScopeLabelCan
writeCreate videosEverything below: start videos, make variants and language versions, save notes.
readRead-onlyFollow, list and download videos, and read usage. Can't start work or spend a video.

Send it on every request as a bearer token:

curl https://demoeditor.com/api/v1/videos \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN"

Keep it on your server: anyone holding a write token can spend your organization's videos.

Errors and limits

Errors come back as JSON with an error string and a matching status code. A refusal for lack of videos also carries a billingUrl to buy a pack.

{ "error": "No videos left. Buy a video pack in Billing." }
StatusMeaningWhat to do
400Invalid bodyThe JSON is malformed, a field is missing or invalid, or an unknown field was sent. The message names it.
401Bad tokenMissing, unknown, revoked or expired token.
402No videos leftThe organization has no video pack with videos left (none bought, all used, expired or refunded), or the action needs a paid pack and only the free trial is left. Buy a pack in Billing.
403Not allowedA read-only token on a write endpoint, the token's user lost the project, or the video was made by someone else.
404Not foundNo such video in the token's project.
409Can't do that nowThe video isn't finished, is being worked on, or is already one of a pair.
429Too manyOver the rate limit, or too many videos in progress (below). Honour the Retry-After header and try again.

Rate limit: 60 requests a minute per token. Polling a video every 30 seconds is well inside it.

Videos in progress: a project can have at most 3 videos in progress at once, and the whole organization at most as many as its plan allows (2 to 6 at once, by pack). A video counts until a poll has seen it finish, or for 2 hours. A video that fails to start, or that ends without a video, is given back to the pack. See Usage for the live numbers.

Start a video

POST/api/v1/videoswrite token

Starts a video from a product's landing page. Only url is required. Unknown fields are refused, so a typo isn't ignored.

NameTypeDescription
urlstringThe product's landing page. The site is read to write the video.
typestringteaser (default), hook (stop the scroll, one idea), pitch or explainer (a product demo).
descriptionstringThe brief for the Director: what the video should say or show. Max 2000 characters.
tonestringThe tone of voice, e.g. "playful". Max 200 characters.
orientationstringportrait (default, 9:16), landscape (16:9) or square (1:1).
languagestringThe video's language, e.g. "en" (default), "es", "ro". See the codes below. Not with languages.
languagesstring[2]Instead of language: the same video in two languages (see Two languages). Not with language or variants: 2.
destinationsstring[]Where it will be posted (tiktok, instagram, youtube, linkedin, x, threads, reddit, website); only steers the copy.
presetstringA preset id (the look of the video). Leave out for the default.
refreshProductbooleanRead the site again instead of reusing notes from an earlier video of the same domain.
variants1 | 22 makes two versions with different openings, for an A/B test (see Two variants).

Uploads (screenshots, reference video, recordings) and GitHub repos are only available in the app for now.

curl -X POST https://demoeditor.com/api/v1/videos \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com", "type": "teaser", "description": "Show the onboarding in 20 seconds" }'

It answers as soon as the video is saved, in about a second (the landing page is read first). 201:

{
  "id": "…",
  "title": "Example Domain",
  "status": "starting",
  "webUrl": "https://demoeditor.com/app/<project>/video?run=…"
}

The video then starts in the background and is made on its own. If it can't start, following it reports status: "failed" with the reason in error. When the platform as a whole is busy, the call still answers 201 with "status": "queued": the video waits in line (it is already charged to the pack) and starts when a place frees up, usually within minutes; following it shows its position. A video that waits more than 6 hours fails and is given back.

Also refused with 402 (no videos left) or 429 (too many in progress).

Language codes: en (English), af (Afrikaans), ar (Arabic), hy (Armenian), as (Assamese), ast (Asturian), az (Azerbaijani), id (Bahasa Indonesia), be (Belarusian), bn (Bengali), bs (Bosnian), bg (Bulgarian), my (Burmese), yue (Cantonese), ca (Catalan), ceb (Cebuano), hr (Croatian), cs (Czech), da (Danish), nl (Dutch), et (Estonian), fil (Filipino), fi (Finnish), fr (French), gl (Galician), ka (Georgian), de (German), el (Greek), gu (Gujarati), ha (Hausa), he (Hebrew), hi (Hindi), hu (Hungarian), is (Icelandic), ga (Irish), it (Italian), ja (Japanese), jv (Javanese), kn (Kannada), kk (Kazakh), ko (Korean), ky (Kyrgyz), lv (Latvian), ln (Lingala), lt (Lithuanian), lb (Luxembourgish), mk (Macedonian), ms (Malay), ml (Malayalam), mt (Maltese), zh (Mandarin Chinese), mi (Māori), mr (Marathi), mn (Mongolian), ne (Nepali), no (Norwegian), oc (Occitan), or (Odia), ps (Pashto), fa (Persian), pl (Polish), pt (Portuguese), pa (Punjabi), ro (Romanian), ru (Russian), sr (Serbian), sd (Sindhi), sk (Slovak), sl (Slovenian), so (Somali), es (Spanish), sw (Swahili), sv (Swedish), tg (Tajik), ta (Tamil), te (Telugu), th (Thai), tr (Turkish), uk (Ukrainian), ur (Urdu), uz (Uzbek), vi (Vietnamese), cy (Welsh), yo (Yoruba).

Two variants

With "variants": 2 the call saves two videos, variant A and variant B, and uses 2 videos from the pack. They are the same video but for the opening, for an A/B test. The 201 is the same, with A as the video, plus both ids:

{
  "id": "<A>",
  "title": "…",
  "status": "starting",
  "webUrl": "…",
  "variants": [{ "id": "<A>", "label": "a" }, { "id": "<B>", "label": "b" }]
}

Variant A writes one storyboard with two openings and makes the shots for both; variant B is made from A once A's shots exist and only assembles its own cut, so the pair takes about as long as one video. Each id is a video of its own: follow and download each one separately.

  • 402 when the organization has fewer than 2 videos left, or only the free trial. Two variants are a paid-pack option.
  • 429 when the pair doesn't fit: it counts as 2 against the organization's videos at once and the project's 3 in progress.
  • While A works, B reports status: "linked", then running and done. If A fails or is cancelled before its shots exist, B fails too and both videos go back to the pack.
  • Following either one adds variant: { label, groupId, otherId } (groupId is A's id).

Two languages

With "languages": ["en", "ro"] (two different codes, in place of language) the call saves two videos the same way: version A in the first language and version B in the second. It uses 2 videos from the pack, answers the same 201 with both ids, and has the same 402 and 429.

B is made from A once A's shots exist: the same shots and look, its voiceover lines and on-screen text translated, voiced in its language and re-timed, then assembled. It finishes after A. B reports linked while A works, then running through voice, shots and assemble, then done. Following either one adds variant.languages, A's first. To add a language to a video that is already finished, see Language version.

Follow it

GET/api/v1/videos/{id}read token

One video's progress. Poll about every 30 seconds until status is done or failed, or just share the webUrl and let your team watch.

{
  "id": "…",
  "title": "Example Domain",
  "status": "running",
  "phase": "storyboard",
  "webUrl": "https://demoeditor.com/app/<project>/video?run=…",
  "videoUrl": "https://demoeditor.com/api/v1/videos/…/video",
  "inputs": { "url": "https://example.com/", "type": "teaser", "description": "…" },
  "note": "Use this one for the launch post",
  "startedAt": "2026-10-20T09:00:00.000Z",
  "updatedAt": "2026-10-20T09:04:12.000Z"
}
statusMeaning
queuedWaiting for the platform's capacity (position is its place in line, 1 = next).
linkedVersion B of a pair, waiting for version A's shots.
startingSaved; its session is starting.
runningBeing made. phase says which step.
countdownThe storyboard is ready; the video is made when the countdown ends, unless someone steps in on the web.
heldWaiting for someone on the web.
doneFinished. videoUrl is set.
failedSee error. Its video goes back to the pack.
cancelledStopped.

videoUrl appears once a cut exists; note is the video's shared note, when it has one. The creator also gets a “your video is ready” email.

Download it

GET/api/v1/videos/{id}/videoread token

The latest cut as video/mp4 (an attachment). 404 until it exists. It needs the bearer token, so fetch it from your server:

curl -L https://demoeditor.com/api/v1/videos/$VIDEO_ID/video \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN" \
  -o video.mp4

List videos

GET/api/v1/videosread token

The token user's videos in the project, newest first. Use Follow it for one video's status.

{
  "items": [
    {
      "id": "…",
      "title": "Example Domain",
      "webUrl": "…",
      "posterUrl": "…",
      "variant": { "label": "a", "groupId": "…" },
      "createdAt": "2026-10-20T09:00:00.000Z"
    }
  ]
}

posterUrl is there once the video has a poster; variant only for a video in a pair.

Note

PUT/api/v1/videos/{id}/notewrite token

Saves the video's shared note, the one your team sees on its page. There is one note per video: it replaces any existing one, and an empty or whitespace-only body clears it. Free: no video is spent.

NameTypeDescription
bodystringThe note's text. Max 2000 characters.
curl -X PUT https://demoeditor.com/api/v1/videos/$VIDEO_ID/note \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Use this one for the launch post" }'
{ "note": "Use this one for the launch post" }

note is null once cleared. 404 for a video that isn't one of the token user's own in the token's project. The note is also part of Follow it.

Variant of a video

POST/api/v1/videos/{id}/variantwrite token

Version B of a finished video, for an A/B test, made after the fact. B is a copy of the video's latest version where only the opening (the first shot or two) changes. It takes 1 video from a paid video pack (never the free trial), and from then on the two are a pair like those made with variants: 2.

NameTypeDescription
anglestring?The idea for the new opening, e.g. "Lead with the price". Max 300 characters. Without it the Director picks an angle that tests something different.
curl -X POST https://demoeditor.com/api/v1/videos/$VIDEO_ID/variant \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "angle": "Lead with the price" }'

201, with B's id. B is a video of its own: follow it by that id.

{
  "id": "<B>",
  "status": "running",
  "webUrl": "…",
  "variant": { "label": "b", "groupId": "<A>", "otherId": "<A>" }
}
  • 403: the token's user didn't make this video. 404: not in the token's project.
  • 409: the video isn't finished, is being worked on, or is already one of a pair.
  • 402: no paid video pack with videos left. 429: the project has 3 videos in progress, or the organization is at its limit.

If anything fails along the way, B is dropped, its video is given back and A is not paired.

Language version

POST/api/v1/videos/{id}/languagewrite token

A finished video in another language. The new video is version B of a language pair, as made with languages: the same shots and look, with the voiceover lines and on-screen text translated, voiced in the new language and re-timed to the new lines. It takes 1 video from a paid video pack (never the free trial).

NameTypeDescription
languagestringThe new language's code, from the list under Start a video. Not the video's own language (400).
curl -X POST https://demoeditor.com/api/v1/videos/$VIDEO_ID/language \
  -H "Authorization: Bearer $DEMOEDITOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "language": "ro" }'

201, with the languages in variant (the video's own first):

{
  "id": "<B>",
  "status": "running",
  "webUrl": "…",
  "variant": { "label": "b", "groupId": "<A>", "otherId": "<A>", "languages": ["en", "ro"] }
}

Same refusals as Variant of a video. A video that is already one of a pair can't get another language. Both videos then report variant.languages when followed.

Usage

GET/api/v1/usageread token

What the token's organization has left to make videos with. Packs are shared by all the organization's projects.

{
  "videosLeft": 7,
  "packs": [
    {
      "name": "Launch",
      "plan": "launch",
      "size": 10,
      "used": 3,
      "remaining": 7,
      "status": "active",
      "purchasedAt": "2026-10-01T08:00:00Z",
      "expiresAt": "2026-11-01T08:00:00Z"
    }
  ],
  "inProgress": { "org": 1, "orgLimit": 3, "project": 1, "projectLimit": 3 }
}
NameTypeDescription
videosLeftnumberThe sum of remaining over the packs whose status is active.
packs[].statusstringactive, used_up, expired or refunded.
inProgress.orgnumberVideos being made now across the organization.
inProgress.orgLimitnumberHow many the organization's plan allows at once (0 without a pack). Over it, new videos are refused with 429.
inProgress.projectnumberVideos being made now in the token's project.
inProgress.projectLimitnumberHow many one project may have at once (3).

MCP: connecting

The MCP server lets AI agents (Claude Code, Cursor, claude.ai, ChatGPT) make Demo Editor videos with the tools below. It is a thin client over this API: every tool calls an endpoint above as you, so scopes, packs and the 402 and 429 refusals all work the same. A refusal comes back as a tool error such as DemoEditor API error 402: No videos left. ….

Server URL (Streamable HTTP):

https://mcp.demoeditor.com/mcp

claude.ai and ChatGPT: add a custom connector and paste the URL. Sign in to Demo Editor when asked, pick a project and allow. There is no token to copy.

Claude Code: use a token.

claude mcp add --transport http demoeditor https://mcp.demoeditor.com/mcp --header "Authorization: Bearer $DEMOEDITOR_TOKEN"

Cursor and other clients: add an HTTP MCP server with the URL and an Authorization: Bearer de_… header.

MCP: access tokens

A token you create in Settings → API works for MCP and for the API alike. A read-only token doesn't get the write tools registered at all.

A connection made from claude.ai or ChatGPT is an ordinary token too: it appears in Settings → API as MCP: <client name> (write, valid for 1 year). Revoking it there ends the connection; the next tool call says to reconnect.

MCP: tools

ToolScopeWhat it does
create_videowriteStarts a video (POST /videos). Inputs: url (required), type, description, tone, orientation, language, languages, destinations, preset, refreshProduct, variants. Answers { id, title, status, webUrl, variants? }.
get_videoreadFollows a video by id: status, phase, position, error, webUrl, videoUrl, variant and note. Poll about every 30 seconds.
list_videosreadThe project's videos, newest first.
get_usagereadVideos left, the packs, and how many videos are in progress against the limits.
set_notewriteInputs id and note. Saves the video's shared note, replacing any existing one; an empty note clears it.
create_variantwriteInputs id and optional angle. Version B of a finished video with a new opening; uses 1 video from a paid pack. Poll get_video on the returned id.
create_language_versionwriteInputs id and language. The finished video in another language; uses 1 video from a paid pack. Poll get_video on the returned id.

A video takes 10 to 30 minutes: poll get_video about every 30 seconds, or share the webUrl. There is no download tool (the mp4 is too large for MCP): share the webUrl, or fetch videoUrl with your token.

Objects

Video (Follow it):

NameTypeDescription
idstringThe video's id.
titlestringIts title.
statusstringqueued, linked, starting, running, countdown, held, done, failed or cancelled.
positionnumber?Place in line (1 = next), while status is queued.
phasestringThe step it is on.
errorstring?Why it failed, when status is failed.
warningsstring[]?What the start left out, once it has started.
webUrlstringOpens the video in the app.
videoUrlstring?The mp4, once a cut exists (needs the bearer token).
inputsobjecturl, type and, when set, description, orientation and language.
variantobject?label (a or b), groupId (A's id), otherId and, for a language pair, languages.
notestring?The shared note, when there is one.
startedAt, updatedAtISO 8601When it started and last changed.

Pack (Usage):

NameTypeDescription
name, planstringThe pack's name and plan id.
sizenumberVideos the pack holds.
used, remainingnumberVideos spent and left.
statusstringactive, used_up, expired or refunded.
purchasedAt, expiresAtISO 8601When it was bought and when it ends.