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/v1A 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:
| Scope | Label | Can |
|---|---|---|
| write | Create videos | Everything below: start videos, make variants and language versions, save notes. |
| read | Read-only | Follow, 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." }| Status | Meaning | What to do |
|---|---|---|
| 400 | Invalid body | The JSON is malformed, a field is missing or invalid, or an unknown field was sent. The message names it. |
| 401 | Bad token | Missing, unknown, revoked or expired token. |
| 402 | No videos left | The 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. |
| 403 | Not allowed | A read-only token on a write endpoint, the token's user lost the project, or the video was made by someone else. |
| 404 | Not found | No such video in the token's project. |
| 409 | Can't do that now | The video isn't finished, is being worked on, or is already one of a pair. |
| 429 | Too many | Over 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
Starts a video from a product's landing page. Only url is required. Unknown fields are refused, so a typo isn't ignored.
| Name | Type | Description |
|---|---|---|
| url | string | The product's landing page. The site is read to write the video. |
| type | string | teaser (default), hook (stop the scroll, one idea), pitch or explainer (a product demo). |
| description | string | The brief for the Director: what the video should say or show. Max 2000 characters. |
| tone | string | The tone of voice, e.g. "playful". Max 200 characters. |
| orientation | string | portrait (default, 9:16), landscape (16:9) or square (1:1). |
| language | string | The video's language, e.g. "en" (default), "es", "ro". See the codes below. Not with languages. |
| languages | string[2] | Instead of language: the same video in two languages (see Two languages). Not with language or variants: 2. |
| destinations | string[] | Where it will be posted (tiktok, instagram, youtube, linkedin, x, threads, reddit, website); only steers the copy. |
| preset | string | A preset id (the look of the video). Leave out for the default. |
| refreshProduct | boolean | Read the site again instead of reusing notes from an earlier video of the same domain. |
| variants | 1 | 2 | 2 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.
402when the organization has fewer than 2 videos left, or only the free trial. Two variants are a paid-pack option.429when 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", thenrunninganddone. 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 }(groupIdis 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
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"
}| status | Meaning | |
|---|---|---|
| queued | Waiting for the platform's capacity (position is its place in line, 1 = next). | |
| linked | Version B of a pair, waiting for version A's shots. | |
| starting | Saved; its session is starting. | |
| running | Being made. phase says which step. | |
| countdown | The storyboard is ready; the video is made when the countdown ends, unless someone steps in on the web. | |
| held | Waiting for someone on the web. | |
| done | Finished. videoUrl is set. | |
| failed | See error. Its video goes back to the pack. | |
| cancelled | Stopped. |
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
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.mp4List videos
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
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.
| Name | Type | Description |
|---|---|---|
| body | string | The 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
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.
| Name | Type | Description |
|---|---|---|
| angle | string? | 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
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).
| Name | Type | Description |
|---|---|---|
| language | string | The 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
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 }
}| Name | Type | Description |
|---|---|---|
| videosLeft | number | The sum of remaining over the packs whose status is active. |
| packs[].status | string | active, used_up, expired or refunded. |
| inProgress.org | number | Videos being made now across the organization. |
| inProgress.orgLimit | number | How many the organization's plan allows at once (0 without a pack). Over it, new videos are refused with 429. |
| inProgress.project | number | Videos being made now in the token's project. |
| inProgress.projectLimit | number | How 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/mcpclaude.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
| Tool | Scope | What it does |
|---|---|---|
| create_video | write | Starts a video (POST /videos). Inputs: url (required), type, description, tone, orientation, language, languages, destinations, preset, refreshProduct, variants. Answers { id, title, status, webUrl, variants? }. |
| get_video | read | Follows a video by id: status, phase, position, error, webUrl, videoUrl, variant and note. Poll about every 30 seconds. |
| list_videos | read | The project's videos, newest first. |
| get_usage | read | Videos left, the packs, and how many videos are in progress against the limits. |
| set_note | write | Inputs id and note. Saves the video's shared note, replacing any existing one; an empty note clears it. |
| create_variant | write | Inputs 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_version | write | Inputs 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):
| Name | Type | Description |
|---|---|---|
| id | string | The video's id. |
| title | string | Its title. |
| status | string | queued, linked, starting, running, countdown, held, done, failed or cancelled. |
| position | number? | Place in line (1 = next), while status is queued. |
| phase | string | The step it is on. |
| error | string? | Why it failed, when status is failed. |
| warnings | string[]? | What the start left out, once it has started. |
| webUrl | string | Opens the video in the app. |
| videoUrl | string? | The mp4, once a cut exists (needs the bearer token). |
| inputs | object | url, type and, when set, description, orientation and language. |
| variant | object? | label (a or b), groupId (A's id), otherId and, for a language pair, languages. |
| note | string? | The shared note, when there is one. |
| startedAt, updatedAt | ISO 8601 | When it started and last changed. |
Pack (Usage):
| Name | Type | Description |
|---|---|---|
| name, plan | string | The pack's name and plan id. |
| size | number | Videos the pack holds. |
| used, remaining | number | Videos spent and left. |
| status | string | active, used_up, expired or refunded. |
| purchasedAt, expiresAt | ISO 8601 | When it was bought and when it ends. |