HTTP API
Everything the dashboard does is an API call. Create videos from JSON or from a topic, export them in any format, and download the files from your own code.
Overview
The API speaks JSON over HTTP. The base address of this server is https://www.bana.video, and every command on this page already uses it. Making a video takes four calls, and the first one can do the work of the first two:
- 1Send the documentWith
POST /api/render, orPOST /api/videosto store it first. It is validated; problems come back as a 422 with their paths. - 2Start the exportUnless you used
render,POST /api/videos/{id}/exportswith the format, shape and resolution you want. You get a job back immediately (202). - 3Wait for the jobPoll
GET /api/jobs/{id}every second or two untilstatusisdone. The first export of a video also records the voice-over, which takes a little longer. - 4Download
GET /api/jobs/{id}/downloadreturns the file.
queued. Jobs and their files are kept until you delete them.Authentication
Every call needs an API token. Make one on the API tokens page of your settings (it is shown once, so copy it then) and send it with every request, as Authorization: Bearer $API_TOKEN or x-api-key: $API_TOKEN. The commands below include the header; set the API_TOKEN variable in your shell first.
A token acts as you: it sees only your videos, files and jobs, and what it does counts against your plan. It can expire, and you can revoke it at any time. Keep it out of web pages and public repositories; tokens start with ve_ so scanners can recognise a leaked one.
Your plan's allowance
Every month your plan allows a number of videos, AI tokens, AI pictures, exports and voice-over recordings, as shown on the Usage page. The API and the dashboard share the same allowance. When one is used up, the call answers 402 with the details, and nothing is started or charged. Work that fails or is cancelled is given back. You can also have only a few exports (and voice-over recordings) in progress at once: more answers 429.
Errors
Errors have a status code and the same body everywhere: a stable code to program against, a message for people and sometimes details.
{
"error": {
"code": "invalid_spec",
"message": "The video JSON has 2 problems.",
"details": {
"errors": [
{
"level": "error",
"path": "scenes[0].items[0].icon",
"message": "Required field is missing."
},
{
"level": "error",
"path": "scenes[0].items[0].say",
"message": "Required field is missing."
}
],
"warnings": []
}
}
}
| Status | code | When |
|---|---|---|
| 400 | bad_request | The request body is missing, too large (over 1 MB), not JSON, or has fields the endpoint does not know. |
| 401 | unauthorized | The request carried no API token, or one that is not valid (unknown, revoked or expired). |
| 402 | quota_exceeded | The monthly allowance of your plan is used up (videos, AI tokens, AI images, exports, voice-overs or premium voice-over). error.details says which (kind), the limit and when it starts over (resetsAt). |
| 404 | not_found | No such video or job. |
| 409 | conflict | The action does not fit the state, such as downloading an export that is still running, or illustrating a video whose scenes all have a picture already. |
| 422 | invalid_spec | The video document has problems. error.details.errors lists each one with its path and a hint. |
| 429 | too_many_requests | You already have as many exports (or voice-over recordings) in progress as are allowed at once. Wait for one to finish, or cancel it. |
| 502 | upstream_error | The AI model did not produce a valid document, even after repairs. error.details.errors has the last problems. |
| 503 | service_unavailable | A part the request needs is not available: the voice server, Chrome, ffmpeg or the AI key (the same key paints pictures). GET /api/health says which. |
Export options
The body of POST /api/videos/{id}/exports, and the options of POST /api/render. Every field is optional; unknown fields are rejected.
| Field | Type | What it does |
|---|---|---|
| format | enum | What to produce.
|
| aspect | enum | Shape of the picture:
|
| resolution | enum | Pixels of the shorter side:
|
| fps | integer | Frames per second. Limits: 10–60 |
| quality | enum | Encoder quality.
|
| captions | boolean | Draw the captions into the picture. Default |
| at | number | For |
| settle | integer | For Limits: 0–10000 |
| music | object | Mix background music under the narration ( |
| └ srcrequired | string | Address of the music: an http(s) link to an MP3, WAV, M4A or OGG file. It loops to fill the video and fades out at the end. Limits: up to 2000 characters |
| └ volume | number | Loudness of the music, 0 to 1, where the narration is 1. Default 0.18. Limits: 0–1 |
| └ duck | boolean | Lower the music further while the narrator speaks. Default |
| callbackUrl | string | When the job finishes (done, failed or cancelled), the job is POSTed to this address as JSON: Limits: up to 2000 characters |
| previewSeconds | number | Render only the first N seconds, to check settings quickly. Limits: 1–3600 |
| format | Produces | Content type | Uses |
|---|---|---|---|
mp4 | H.264 + AAC. Plays everywhere: social media, phones, slides. | video/mp4 | Voice-over and picture |
webm | VP9 + Opus. Smaller files, ideal for the web. | video/webm | Voice-over and picture |
mov | H.264 + AAC in a QuickTime container, for Final Cut and iMovie. | video/quicktime | Voice-over and picture |
gif | Silent loop, limited to 480p and 15 fps. | image/gif | Voice-over and picture |
mp3 | Just the narration, for podcasts. | audio/mpeg | The voice-over |
png | One frame, for thumbnails (set `at`). | image/png | Voice-over and picture |
srt | Captions timed to the narration. | application/x-subrip | The script only |
vtt | Captions for HTML5 video players. | text/vtt | The script only |
json | The spec this video was made from. | application/json | The script only |
resolutionnames the pixels of the shorter side.1080pwith9:16is 1080×1920.gifis capped at 480p and 15 fps and has no sound.pngcaptures one frame, atatseconds.previewSecondsrenders only the start of the video: a quick way to check a setting before the full export.musicmixes background music intomp4,webm,movandmp3:{ "src": "https://…/song.mp3", "volume": 0.18, "duck": true }. The file is downloaded once and cached, loops to fill the video, fades in and out, and withduckgets quieter whenever the narrator speaks. Without it, an export uses themusicsetting of the video itself, the one the editor writes. A video withsound(sound effects and a music bed made for it) has them mixed in as well, and the whole mix is mastered to about −15 LUFS.callbackUrlsaves you from polling: when the job ends (done, failed or cancelled) the server POSTs{ "event": "job.finished", "job": … }to it. A receiver that is down does not affect the job.
A complete example
Make a vertical 1080p MP4 from a document, wait for it and save it. The scripts poll the job every two seconds. Run any of them and you will find the video file next to it.
#!/usr/bin/env bash
# Needs curl and jq.
set -euo pipefail
BASE=https://www.bana.video
JOB=$(curl -sf -X POST "$BASE/api/render" -H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" --data-binary @- <<'JSON' | jq -r .job.id
{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p"
}
}
JSON
)
while true; do
STATUS=$(curl -sf "$BASE/api/jobs/$JOB" -H "Authorization: Bearer $API_TOKEN")
echo "$(echo "$STATUS" | jq -r '.job.phase') $(echo "$STATUS" | jq -r '(.job.progress * 100 | floor)')%"
case "$(echo "$STATUS" | jq -r .job.status)" in
done) break ;;
failed|cancelled) echo "$STATUS" | jq -r .job.error; exit 1 ;;
esac
sleep 2
done
curl -sf -OJL "$BASE/api/jobs/$JOB/download" -H "Authorization: Bearer $API_TOKEN"
Endpoints
Document to file in one call
The fastest way from JSON to a video file: render stores the document and starts the export. Poll the job, then download.
/api/renderCreate a video and start exporting it
options is optional and takes everything listed under Export options. The voice-over is recorded first when needed.
{
"spec": "{ … the document … }",
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30
}
}
curl -X POST https://www.bana.video/api/render \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30
}
}'
202Accepted. Location points to the job.
{
"video": "{ … }",
"job": {
"id": "75ae15a72b77b7b1",
"kind": "export",
"videoId": "b921df158655611f",
"status": "running",
"phase": "Drawing frames",
"progress": 0.42,
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30,
"quality": "standard",
"captions": true
},
"error": null,
"file": null,
"createdAt": "2026-10-12T21:38:44.157Z",
"startedAt": "2026-10-12T21:38:44.159Z",
"finishedAt": null
}
}
422The document has problems.
/api/jobs/{id}Follow a job
status is queued, running, done, failed or cancelled. progress goes from 0 to 1 and phase says what is happening. When done, file describes the result.
curl https://www.bana.video/api/jobs/75ae15a72b77b7b1 \
-H "Authorization: Bearer $API_TOKEN"
200The job.
{
"job": {
"id": "75ae15a72b77b7b1",
"kind": "export",
"videoId": "b921df158655611f",
"status": "done",
"phase": "Done",
"progress": 1,
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30,
"quality": "standard",
"captions": true
},
"error": null,
"file": {
"name": "output.mp4",
"mime": "video/mp4",
"bytes": 5836270
},
"createdAt": "2026-10-12T21:38:44.157Z",
"startedAt": "2026-10-12T21:38:44.159Z",
"finishedAt": "2026-10-12T21:39:08.640Z"
}
}
404No such job.
/api/jobs/{id}/downloadDownload the finished file
Answers with a redirect to a short-lived address of the file in storage (which sends it as an attachment named after the video title, and supports Range requests), so follow redirects (curl -L). Answers 409 until the job is done.
curl -OJL https://www.bana.video/api/jobs/75ae15a72b77b7b1/download \
-H "Authorization: Bearer $API_TOKEN"
307Redirect to the file. The address works for a few minutes; ask again for a new one.
409The export is not finished.
Writing with the AI
Turn a topic, or a web page, into a video document, and compose the music under it. The result is checked by the validator, and repaired by the model when it has problems.
/api/sourcesRead the words of a web page
Reads a public web page (or a plain text or Markdown file) and answers with its title and text, without the menus, footers and side panels: put text in source of POST /api/generate to base a video on the page. Only http and https addresses on the usual ports that lead to the public internet can be read: addresses on a private network, on this server or in a cloud metadata service are refused, also by redirect. The page may take up to 10 seconds to answer, and its first 3 MB are read. text has at most 20,000 characters; truncated says when the page had more. Pages that need a sign-in, build themselves in the browser, or are not text (PDF, images) cannot be read. No credit or token is used.
{
"url": "https://en.wikipedia.org/wiki/Photosynthesis"
}
curl -X POST https://www.bana.video/api/sources \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://en.wikipedia.org/wiki/Photosynthesis"
}'
200The page, as source.
{
"source": {
"url": "https://en.wikipedia.org/wiki/Photosynthesis",
"title": "Photosynthesis",
"text": "Photosynthesis is the process by which …",
"truncated": true
}
}
400The address is not a web address, leads somewhere that cannot be read (a private network, a sign-in page), is not a page or a text file, or has hardly any words.
502The page could not be reached, took too long, or answered with an error.
/api/generateWrite a video document from a topic
Only topic is required. format is the kind of video (explainer, lesson, howto, revision, quiz, short, tips, story, travel, invite, memories, promo, demo, offer, review, casestudy, training, update or pitch): it tells the writer what the video is for, and sets the shape, the length and the look unless you give them. aspect is the shape: 16:9 (default), 4:3, 1:1, 4:5, 3:4 or 9:16 for Shorts, Reels and TikTok; it is kept in the document, and the editor, the watch page, embeds and exports start from it. language is any narration language code (default en). length is 30s, 1m, 3m (the default), 5m or 10m; speed is the voice speed from 0.5 to 2 (default 1): it is set as narration.speed, and the script is sized so the video still runs for length. engine is the kind of voice-over: standard (default) is the voice server, and voice is who speaks (M1 to M5, F1 to F5; default F2), set as narration.voice; premium is Google Gemini 3.8 Flash TTS, directed by the writer: premium picks one of the 30 voices, a style, an accent and a pace (slow, natural or brisk; what you leave out the writer chooses to suit the topic), and every scene gets its own delivery. Put your own narration in script (up to 9,000 characters) and the video says exactly those words, in that order: the AI only divides it into scenes and designs the screen of each, so the length follows from the script and length is not needed (not together with source; topic can then be a short title). If the AI cannot keep every word, the answer still comes, with a warning first in warnings. Put an article, notes or a transcript in source and the script sticks to the facts in it (POST /api/sources reads a web page into that text). With save: true the video is also stored and returned as video. pictures (up to 4) hands the AI pictures to work from, such as a photo of a whiteboard, a chart or a product shot: upload each one first (POST /api/assets?kind=image) and send its url as src. The AI reads what they show, and with show: true on a picture it also puts it in the video. The call can take up to a minute, a few minutes for a ten-minute video.
{
"topic": "How vaccines train the immune system",
"format": "explainer",
"description": "Explain it to teenagers. Include one quiz question.",
"audience": "teenagers",
"tone": "friendly",
"language": "en",
"length": "3m",
"aspect": "16:9",
"speed": 1,
"engine": "standard",
"voice": "F2",
"pictures": [
{
"src": "/api/assets/3c9d0e5a71b2f486",
"show": false
}
],
"save": true
}
curl -X POST https://www.bana.video/api/generate \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic": "How vaccines train the immune system",
"length": "30s",
"save": true
}'
200The document, the model used and how many attempts it took.
{
"spec": "{ … }",
"warnings": [],
"attempts": 1,
"model": "anthropic/claude-haiku-5.5",
"tokens": {
"prompt": 5200,
"completion": 2400
},
"video": "{ … } or null"
}
400A picture in pictures is not one of your files (any more).
502The model could not produce a valid document.
503No AI key is configured, or pictures were sent and the model in use cannot look at pictures.
/api/reviseChange a document as asked, or translate it
Send the current document and an instruction in plain words ("make it shorter", "add a quiz before the end"), a language to translate the whole video into, or both. Add scene (counting from 0) to change only that scene: the editor's “Edit this scene with AI”. With a scene, annotations (up to 8) pin notes on its picture, as the editor does when you click on it: x and y say where (0 to 1 across and down the picture), note what to change there, and path and text (optional) which part of the scene is there (items[1].text) and the words on it, so that the AI knows what you mean. pictures (up to 4) hands the AI pictures to look at while it makes the change, such as a photo to use, a chart to copy or a sketch to follow: upload each one first (POST /api/assets?kind=image) and send its url as src; with show: true the AI also puts the picture in the video. Everything the change does not touch stays as it is. The result is checked and repaired like a new document, and it is not stored: send it to PUT /api/videos/{id} to keep it.
{
"spec": "{ … the current document … }",
"instruction": "Add a line about common mistakes",
"scene": 2,
"annotations": [
{
"note": "Say this more simply",
"x": 0.42,
"y": 0.31,
"path": "items[1].text",
"text": "Photosynthesis converts photons into sugar"
}
],
"pictures": [
{
"src": "/api/assets/3c9d0e5a71b2f486",
"show": false
}
]
}
curl -X POST https://www.bana.video/api/revise \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"instruction": "Add a scene about sunsets"
}'
200The changed document, in the shape of /api/generate without video.
{
"spec": "{ … }",
"warnings": [],
"attempts": 1,
"model": "anthropic/claude-haiku-5.5",
"tokens": {
"prompt": 9100,
"completion": 2600
}
}
400Neither instruction, language nor annotations was given, or scene was combined with language, or has neither an instruction nor notes, or annotations has no scene; or a picture in pictures is not one of your files (any more).
422The document you sent has problems. Fix them first.
502The model could not produce a valid document.
503No AI key is configured, or pictures were sent and the model in use cannot look at pictures.
/api/publishWrite social post copy for a video
Send a video document and, optionally, which platforms to write for (youtube, tiktok, instagram, linkedin, x; default: all five). The AI writes titles and captions from the video's own words only. Nothing is stored.
{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"platforms": [
"youtube",
"linkedin"
]
}
curl -X POST https://www.bana.video/api/publish \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"platforms": [
"youtube",
"x"
]
}'
200Post copy per platform, plus model and tokens.
{
"platforms": {
"youtube": {
"title": "Why the sky is blue",
"body": "Sunlight hides every colour…",
"hashtags": [
"science",
"sky",
"learning"
]
}
},
"model": "anthropic/claude-haiku-5.5",
"tokens": {
"prompt": 900,
"completion": 400
}
}
422The document has problems.
502The model did not return usable JSON.
503No AI key is configured.
/api/musicCompose music for a video
The AI composes an instrumental track, without vocals and kept quiet enough for a voice-over to sit on, and keeps it as an audio file of yours. mood is calm (default), uplifting, energetic, cinematic, playful, corporate, tense or chill; description adds your own words ("with a Spanish guitar"); length is clip (default: about 30 seconds, repeated under the video) or song (about 2 minutes, for longer videos). Set the url of the returned file as music.src of the document to use it. It takes 10 to 60 seconds, and counts as 50,000 (clip) or 100,000 (song) of your monthly AI tokens.
{
"mood": "uplifting",
"description": "with a light acoustic guitar",
"length": "clip"
}
curl -X POST https://www.bana.video/api/music \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mood": "uplifting",
"length": "clip"
}'
200The track, as an audio file of yours.
{
"asset": {
"id": "8d1f6a02c3b94e57",
"kind": "audio",
"name": "Uplifting music.mp3",
"url": "/api/assets/8d1f6a02c3b94e57",
"bytes": 481232,
"durationSeconds": 31.2,
"prompt": null,
"transparent": false,
"createdAt": "2026-10-16T09:12:40.031Z"
}
}
402Your plan has no AI tokens left this month.
502The music generator failed or did not return a track.
503No AI key is configured.
Pictures with the AI
Paint pictures with the AI image model, or let an art director plan them for a whole video. Pictures are painted together on sprite sheets (a grid of up to 5 × 5 per request to the model), cut apart and kept as your images. Each picture that comes out costs one image credit; one that does not is free. Put a picture on a scene with the image field of the scene.
/api/imagesPaint pictures on sprite sheets
images lists what to paint (up to 100). They are laid out on sheets of at most 25: 12 pictures make one 3 × 4 sheet, 30 make a 25 and a 5. Set grid (at most 5 × 5) to choose the grid, or square: true to keep it square (12 pictures then use a 4 × 4 sheet with 4 empty cells). maxPerSheet puts fewer pictures on each sheet, which makes them sharper (the editor uses 12). The model is asked to separate the cells with magenta dividers, and the pictures are cut along them: a sheet whose cells cannot be told apart is painted again (up to three tries), and a picture whose subject is cut off by the edge of its cell does not come out (and is not charged). style sets one look for all of them (photo, cinematic, illustration, 3d, isometric, watercolor, anime, pixel, sketch, paper, neon), aspect their shape. A picture with background green or black is painted as one subject on a flat screen of that colour, which is removed afterwards, so the result is transparent. The call can take a few minutes: ask for at most two sheets at a time.
{
"images": [
{
"id": "lighthouse",
"prompt": "A white lighthouse on a rocky coast at sunrise, waves below"
},
{
"id": "gull",
"prompt": "A seagull in flight, wings spread",
"background": "green"
}
],
"style": "photo",
"aspect": "16:9"
}
curl -X POST https://www.bana.video/api/images \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"images": [
{
"id": "lighthouse",
"prompt": "A white lighthouse on a rocky coast at sunrise, waves below"
}
],
"style": "photo"
}'
200The pictures that came out (each is an image of yours, with its url), the ones that did not, the sheets and what was charged.
{
"images": [
{
"id": "lighthouse",
"asset": {
"id": "3c9d0e5a71b2f486",
"kind": "image",
"name": "lighthouse.webp",
"url": "/api/assets/3c9d0e5a71b2f486",
"bytes": 182113,
"prompt": "A white lighthouse on a rocky coast at sunrise, waves below",
"transparent": false
}
}
],
"failed": [
{
"id": "gull",
"reason": "The model left this picture out."
}
],
"sheets": [
{
"grid": {
"cols": 2,
"rows": 1
},
"pictures": 2,
"background": "scene",
"ok": true,
"error": null
}
],
"model": "inclusionai/ming-image-0.1-design",
"costUsd": null,
"charged": 1,
"remaining": 99
}
400A field is not valid: a grid over 5 × 5, more than 100 pictures, a repeated id, or an unknown style.
402Your plan has fewer image credits left than pictures asked for. Nothing is painted.
503No AI key is configured.
/api/images/planPlan the pictures of a video
An art director reads the document and proposes which scenes get a picture, as a backdrop behind the scene, a transparent cutout beside the text, or a scene of its own, with a prompt for each and one look for the whole video. style is auto (default, the director chooses) or one of the looks above; amount is few, balanced (default) or lots. Nothing is painted and no image credit is used, only AI tokens; the plan never asks for more pictures than you have credits left. Paint it with POST /api/images, then set the image of each scene.
{
"spec": "{ … the document … }",
"style": "auto",
"amount": "balanced"
}
curl -X POST https://www.bana.video/api/images/plan \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
},
"amount": "balanced"
}'
200The plan, the credits left and the model used.
{
"plan": {
"style": "photo",
"pictures": "{ … one per scene that gets a picture … }"
},
"remaining": 100,
"model": "anthropic/claude-haiku-5.5",
"tokens": {
"prompt": 4100,
"completion": 900
}
}
409Every scene has a picture already.
422The document has problems.
402No image credits are left this month.
503No AI key is configured.
/api/assetsList your pictures, recordings or clips
The newest first. kind is image (default), audio or video (footage for clip scenes); limit is 1 to 100 (default 60). Pictures painted by the AI carry the prompt they were made from, and transparent is true for a cut-out.
curl https://www.bana.video/api/assets?kind=image&limit=20 \
-H "Authorization: Bearer $API_TOKEN"
200{ assets: [ … ] }, in the shape of an uploaded file.
/api/usageYour plan and what is left of this month
The allowance of every kind (video, ai_tokens, image, export, voiceover, premium_voice): its limit, what is used and what is remaining, and when the month starts over. premium_voice is counted in seconds (the Free plan has 180), the rest in pieces.
curl https://www.bana.video/api/usage \
-H "Authorization: Bearer $API_TOKEN"
200{ summary }, as shown on the Usage page.
Product ads
Make a video ad for a product, an app or a business, in three calls that can be used on their own: read the website for the facts, read and prepare the logo and pictures, then compose the ad. The composer chooses and orders the scenes, places the pictures, and dresses the video in the brand's colour; the AI only writes the words, from the facts you give it. Save the result with save: true or send spec to POST /api/videos, then edit it like any other video.
/api/product/crawlFind out about a product from its web page
Reads the page (and up to two pages of the same site that tell more: pricing, features, about) and answers with a brief to check and complete, the pictures it found, and notes on what is missing. The page's own data comes first (structured data, Open Graph tags, the browser colour); when the AI is set up it also reads the words of the page, and only what the page backs up is kept: a number the page does not contain, or a quote that is not on it word for word, is dropped. A rating is only taken from the page's structured data, never from the AI. A site's robots.txt is respected (this crawler is called videobana): a page it asks robots to keep away from is refused. Only http and https addresses that lead to the public internet can be read. It uses a little of your AI tokens, and no credit.
{
"url": "https://aurora.audio/one"
}
curl -X POST https://www.bana.video/api/product/crawl \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://aurora.audio/one"
}'
200What the page says, as a brief; the pictures found, each marked as the page's logo or one of its images; where the facts came from (found); what could not be found (notes); and whether the AI read the words (ai).
{
"url": "https://aurora.audio/one",
"brief": {
"name": "Aurora One",
"kind": "product",
"tagline": "Wireless headphones that disappear.",
"features": [
{
"title": "40-hour battery"
},
{
"title": "Active noise cancelling"
}
],
"price": {
"now": "$249"
},
"cta": {
"link": "aurora.audio"
},
"website": "https://aurora.audio/one"
},
"images": [
{
"url": "https://aurora.audio/logo.svg",
"hint": "logo"
},
{
"url": "https://aurora.audio/img/front.jpg",
"hint": "image"
}
],
"found": [
"structured data",
"price",
"logo",
"pictures",
"features (from the words of the page)"
],
"notes": [],
"ai": true
}
400The address is not a web address, leads somewhere that cannot be read, is not a page, or the site asks robots not to read it.
502The page could not be reached, took too long, or answered with an error.
/api/product/picturesRead the logo and pictures of a product
Takes up to 8 sources, each the url of a picture on the web (copied into your files, at most 8 MB) or the src of a picture of yours (POST /api/assets?kind=image). Each is read for what it is (logo, product, screenshot or photo: give role when you know, or hint with what the page called it: a picture the page shows as one of its images is never taken for a logo), whether it is see-through, where the subject is, its main colours and how light it is. A logo or product on a flat background is cut out of it so it can float on the stage (cutOut: false to keep it whole), and the cut-out is kept as a new file; an SVG logo is drawn as a picture, and refused when it loads other files. Give the product's features (in order) and, with understand on (the default), the AI also looks at the main product picture, says what it shows and where each feature is, so the video can point at it. The pictures come back in the order sent, with a note for any that could not be used; every src returned is a file of yours.
{
"sources": [
{
"url": "https://aurora.audio/logo.svg",
"hint": "logo"
},
{
"url": "https://aurora.audio/img/front.jpg",
"hint": "image"
}
],
"features": [
"40-hour battery",
"Active noise cancelling"
],
"cutOut": true,
"understand": true
}
curl -X POST https://www.bana.video/api/product/pictures \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sources": [
{
"url": "https://aurora.audio/img/front.jpg",
"hint": "image"
}
],
"features": [
"40-hour battery",
"Active noise cancelling"
]
}'
200pictures (each with the src to use, its name, where it was copied from, its analysis and whether its background was cut away) and notes. Change an analysis.role before composing to tell the composer what a picture is.
{
"pictures": [
{
"src": "/api/assets/5b1e07a3c9d24f68",
"name": "front.webp",
"source": "https://aurora.audio/img/front.jpg",
"analysis": {
"width": 1600,
"height": 1600,
"transparent": true,
"plainBackground": true,
"subject": {
"x": 8,
"y": 20,
"width": 84,
"height": 60
},
"colors": [
{
"hex": "#282828",
"share": 0.61
}
],
"brightness": 0.39,
"role": "product",
"description": "Black over-ear headphones on a plain background"
},
"cutOut": true
}
],
"notes": []
}
400None of the pictures could be used (the notes say why), or a src is not one of your files (any more).
/api/product/composeCompose a video ad from a brief and its pictures
Only brief.name is required; every other fact is optional, and a part of the ad that the facts cannot support is left out (and listed in plan.skipped, with what would have helped in plan.missing) instead of being made up. brief holds the name, kind (product, app or service), category (the sort of business: it picks the usual recipe and look), tagline, description, problem, features, benefits, steps, lineup (up to 6 things sold, each with a name, a line, a price, a tag and a picture of yours: with pictures they become a gallery, with prices a price list), specs (up to 6 key facts, each a label and a value), integrations (up to 8: what it works with), event (a title, a date with day and month, and a time, place, host and up to 3 perks), price, offer, rating, badges, testimonials (up to 6), cta, stores, website, brandColor and language. Prices, ratings and numbers are shown exactly as given: the ad never works one out. pictures are those returned by POST /api/product/pictures. recipe is the shape of the ad (launch, scroll-stopper, spotlight, app-demo, flash-sale, before-after, service-intro, social-proof, catalog, menu-special, listing, event-promo, saas-pitch, local-service, course, wall-of-love): by default the one that fits the brief and pictures best. aspect, length (15s to 60s: the least important parts are left out to fit), voice (one of the 30 premium voices: an ad is always narrated by the premium voice, and by default it is cast to suit the business) and theme override the recipe's. The colours of the video follow brief.brandColor, else the strongest colour of the logo (useBrandColor: false keeps the look of the theme). When the AI is set up it writes the words in the language of the brief; every line is checked against the brief, and one that states a number or a claim you did not give is replaced by a line made from the facts (copy.rejected lists them). Without the AI, or when it fails, the words are made from the facts in English (with a warning for another language). With save: true the video is also stored and returned as video.
{
"brief": {
"name": "Aurora One",
"kind": "product",
"tagline": "Wireless headphones that disappear.",
"features": [
{
"title": "40-hour battery"
},
{
"title": "Active noise cancelling"
}
],
"benefits": [
"Comfortable all day"
],
"price": {
"now": "$249",
"was": "$299"
},
"cta": {
"button": "Shop now",
"link": "aurora.audio"
},
"brandColor": "#0d9488"
},
"pictures": [
{
"src": "/api/assets/5b1e07a3c9d24f68",
"analysis": "{ … as returned by /api/product/pictures … }"
}
],
"recipe": "launch",
"length": "30s",
"save": true
}
curl -X POST https://www.bana.video/api/product/compose \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"brief": {
"name": "Aurora One",
"kind": "product",
"features": [
{
"title": "40-hour battery"
},
{
"title": "Active noise cancelling"
}
],
"price": {
"now": "$249"
}
},
"recipe": "launch",
"length": "30s"
}'
200The document (spec), what was made and what was left out (plan), where the words came from (copy), problems found by the validator (warnings) and the stored video or null.
{
"spec": "{ … }",
"plan": {
"recipe": "launch",
"aspect": "16:9",
"seconds": 28.4,
"budget": 30,
"scenes": [
{
"slot": "hook",
"kind": "kinetic"
}
],
"skipped": [
{
"slot": "offer",
"reason": "there is no offer"
}
],
"shortened": false,
"missing": [
"offer"
],
"brandColor": "#0d9488",
"unusedPictures": []
},
"copy": {
"source": "ai",
"language": "en",
"rejected": []
},
"warnings": [],
"video": "{ … } or null"
}
400The brief or a picture is not valid, or a picture is not one of your files.
402Your plan has no AI tokens left this month (or no video left, with save).
422The composed document has problems. This should not happen: please report it with your request.
Videos
Stored videos: a document, its voice-over and its exports. Ids are 16 hexadecimal characters.
/api/validateCheck a document without storing it
Returns every error and warning with its path and often a hint, plus stats for a valid document.
{
"spec": "{ … the document … }"
}
curl -X POST https://www.bana.video/api/validate \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}'
200Always 200; read valid.
{
"valid": true,
"errors": [],
"warnings": [
{
"level": "warning",
"path": "scenes[0]",
"message": "The video starts without a title card.",
"hint": "Start with { \"kind\": \"intro\", ... }"
}
],
"stats": {
"scenes": 3,
"sentences": 4,
"words": 38,
"estimatedSeconds": 21
}
}
/api/videosStore a video
{
"spec": "{ … the document … }"
}
curl -X POST https://www.bana.video/api/videos \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}'
201Created. Location points to the video.
{
"video": {
"id": "b921df158655611f",
"title": "Why the sky is blue",
"domain": "science",
"scenes": 3,
"durationSeconds": 21.4,
"look": {
"colors": [
"#06283d",
"#0e7490",
"#4338ca"
],
"emoji": "🌧️"
},
"visibility": "private",
"createdAt": "2026-10-12T21:35:31.303Z",
"updatedAt": "2026-10-12T21:35:31.303Z",
"narration": {
"status": "missing",
"durationSeconds": null
},
"spec": "{ … the document … }"
}
}
422The document has problems.
/api/videosList videos, newest first
curl https://www.bana.video/api/videos \
-H "Authorization: Bearer $API_TOKEN"
200Light summaries, without documents.
{
"videos": [
{
"id": "b921df158655611f",
"title": "Why the sky is blue",
"domain": "science",
"scenes": 3,
"durationSeconds": 21.4,
"look": {
"colors": [
"#06283d",
"#0e7490",
"#4338ca"
],
"emoji": "🌧️"
},
"visibility": "private",
"createdAt": "2026-10-12T21:35:31.303Z",
"updatedAt": "2026-10-12T21:35:31.303Z"
}
]
}
/api/videos/{id}Read a video with its document
narration.status is silent, missing (not recorded), outdated (the script or voice changed since) or ready.
curl https://www.bana.video/api/videos/b921df158655611f \
-H "Authorization: Bearer $API_TOKEN"
200The video.
{
"video": {
"id": "b921df158655611f",
"title": "Why the sky is blue",
"domain": "science",
"scenes": 3,
"durationSeconds": 21.4,
"look": {
"colors": [
"#06283d",
"#0e7490",
"#4338ca"
],
"emoji": "🌧️"
},
"visibility": "private",
"createdAt": "2026-10-12T21:35:31.303Z",
"updatedAt": "2026-10-12T21:35:31.303Z",
"narration": {
"status": "missing",
"durationSeconds": null
},
"spec": "{ … the document … }"
}
}
404No such video.
/api/videos/{id}Replace a video's document
The id stays. The voice-over is recorded again only when the spoken text or the voice changed, and then only for the sentences that changed.
{
"spec": "{ … the new document … }"
}
curl -X PUT https://www.bana.video/api/videos/b921df158655611f \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}'
200The updated video.
422The document has problems.
/api/videos/{id}/visibilityMake a video public or private
New videos are private. Changing the document does not change visibility. Public videos can be watched at /watch/{id} and embedded; private ones answer 404 to anonymous visitors.
{
"visibility": "public"
}
curl -X PUT https://www.bana.video/api/videos/b921df158655611f/visibility \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"visibility": "public"
}'
200The video with its new visibility.
{
"video": {
"id": "b921df158655611f",
"title": "Why the sky is blue",
"domain": "science",
"scenes": 3,
"durationSeconds": 21.4,
"look": {
"colors": [
"#06283d",
"#0e7490",
"#4338ca"
],
"emoji": "🌧️"
},
"visibility": "public",
"createdAt": "2026-10-12T21:35:31.303Z",
"updatedAt": "2026-10-12T21:35:31.303Z",
"narration": {
"status": "missing",
"durationSeconds": null
},
"spec": "{ … the document … }"
}
}
404No such video.
/api/videos/{id}/duplicateCopy a video
The copy has a new id and the title … (copy). Change it freely: the original is not touched.
curl -X POST https://www.bana.video/api/videos/b921df158655611f/duplicate \
-H "Authorization: Bearer $API_TOKEN"
201The new video. Location points to it.
{
"video": {
"id": "0f3a9c61d2b84e57",
"title": "Why the sky is blue (copy)",
"domain": "science",
"scenes": 3,
"durationSeconds": 21.4,
"look": {
"colors": [
"#06283d",
"#0e7490",
"#4338ca"
],
"emoji": "🌧️"
},
"visibility": "private",
"createdAt": "2026-10-12T21:35:31.303Z",
"updatedAt": "2026-10-12T21:35:31.303Z",
"narration": {
"status": "missing",
"durationSeconds": null
},
"spec": "{ … the document … }"
}
}
404No such video.
/api/videos/{id}Delete a video and all of its exports
curl -X DELETE https://www.bana.video/api/videos/b921df158655611f \
-H "Authorization: Bearer $API_TOKEN"
204Deleted.
/api/videos/{id}/narrationRecord the voice-over
Optional: exports do this on their own. Useful to hear the result first, or to have it ready before a burst of exports.
curl -X POST https://www.bana.video/api/videos/b921df158655611f/narration \
-H "Authorization: Bearer $API_TOKEN"
202The job to follow with GET /api/jobs/{id}.
503The voice server is not reachable.
/api/videos/{id}/audioThe recorded narration as MP3
Redirects to the file in storage, which supports Range requests, so players can seek. Answers 404 until the voice-over is ready.
curl -L -o narration.mp3 https://www.bana.video/api/videos/b921df158655611f/audio \
-H "Authorization: Bearer $API_TOKEN"
307Redirect to the audio (audio/mpeg).
404Not recorded, or out of date.
/api/narration/draftRecord the voice-over of a document that is not saved yet
What the editor's “Generate voice-over” button calls. The body is { "spec": <video document> }. Only the spoken words and the voice are recorded, so the recording is found again by its fingerprint and re-used for any look of the same script. Saving the video later picks it up, without recording again. Answers 200 with the draft when it exists already, otherwise 202 with the job to follow.
{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}
curl -X POST https://www.bana.video/api/narration/draft \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}'
200Already recorded: { draft: { fingerprint, timings, audioUrl }, job: null }.
202Recording. { draft: null, job }; follow it with GET /api/jobs/{id}, then send the same request again to get the draft.
409The document is silent (narration.enabled is false), so there is nothing to record.
422The video document has problems.
/api/narration/draft/lookupIs the voice-over of a document recorded already?
Only looks, never records. The editor asks this when you load an example or change the words, so a voice-over that exists is there at once. The examples of the gallery ship with theirs.
{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}
curl -X POST https://www.bana.video/api/narration/draft/lookup \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"title": "Why the sky is blue",
"domain": "science",
"scenes": [
{
"kind": "intro",
"say": [
"Why is the sky blue?",
"The answer is hiding in sunlight."
]
},
{
"kind": "statement",
"text": "Sunlight is made of **every colour** at once.",
"say": "Sunlight is made of every colour at once."
},
{
"kind": "statement",
"text": "Blue light scatters the **most**, so blue is the colour we see.",
"say": "Blue light scatters the most, so blue is the colour we see."
}
]
}
}'
200{ draft: { fingerprint, timings, audioUrl } }, or { draft: null } when there is none.
422The video document has problems.
/api/narration/draft/{fingerprint}/audioA recorded draft voice-over as MP3
Redirects to the recording, which supports Range requests. The audioUrl of a draft points here.
curl -L -o draft.mp3 https://www.bana.video/api/narration/draft/3cc982906520/audio \
-H "Authorization: Bearer $API_TOKEN"
307Redirect to the audio (audio/mpeg).
404No such recording.
Files
Recordings and music uploaded from the editor, or from your own code, to use in a document: narration.voiceover.src for your own voice, music.src for the music, brand.logo for a logo.
/api/assetsUpload a small audio file, image or clip
For files up to 4 MB (the most a request to this server can carry); for larger files use the two steps below. The body is the raw file, not JSON. kind (audio, image or video) goes in the query, and the original name in the X-File-Name header, percent-encoded. Audio of any common format is converted to MP3; images are checked by their content. A video (MP4, MOV or WebM, from half a second to a minute) is prepared as a silent MP4 with a poster still, which GET /api/assets/{id}/poster returns: use its url as the src of a clip scene. Footage is nearly always larger than 4 MB, so send it in the two steps below.
curl -X POST "https://www.bana.video/api/assets?kind=audio" \
-H "Authorization: Bearer $API_TOKEN" \
-H "X-File-Name: voice.m4a" \
--data-binary @voice.m4a
201The file. Use its url in a document.
{
"asset": {
"id": "9f2c1d7a40b3e865",
"kind": "audio",
"name": "voice.m4a",
"url": "/api/assets/9f2c1d7a40b3e865",
"bytes": 482113,
"durationSeconds": 21.4
}
}
400kind is missing, the file is empty, or not what kind says.
413The file is too large for this way of uploading (or over the limit of its kind: 60 MB of audio, 8 MB of an image).
/api/assets/uploadsUpload a file of any size, step 1: get an address to send it to
Tell what the file is and how many bytes it has. You get an upload.url to PUT the raw file to (with the headers given, and no Authorization header), straight to file storage. The address works for ten minutes. Then finish with step 2. Up to 60 MB of audio, 8 MB of an image or 100 MB of a video clip.
{
"kind": "audio",
"bytes": 482113
}
curl -X POST https://www.bana.video/api/assets/uploads \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "audio",
"bytes": 482113
}'
201Where to send the file.
{
"upload": {
"uploadId": "5d0e7a93c1b2f468",
"url": "https://…",
"headers": {
"Content-Type": "application/octet-stream"
}
}
}
400bytes is not a positive whole number.
413The file is larger than the limit of its kind.
/api/assets/uploads/{uploadId}Upload a file of any size, step 2: finish
Once the PUT of step 1 is done, this checks the file, converts audio to MP3 and keeps it as an asset. name is the original file name. Use the url of the asset in a document.
{
"kind": "audio",
"name": "voice.m4a"
}
curl -X POST https://www.bana.video/api/assets/uploads/5d0e7a93c1b2f468 \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "audio",
"name": "voice.m4a"
}'
201The file, as in the answer of the upload above.
400The file did not arrive, or is not what kind says.
/api/assets/{id}An uploaded file
Redirects to the file in storage, which supports Range requests, so players can seek. Only your own files can be read.
curl -L -o voice.mp3 https://www.bana.video/api/assets/9f2c1d7a40b3e865 \
-H "Authorization: Bearer $API_TOKEN"
307Redirect to the file.
404No such file.
/api/assets/{id}/posterThe poster still of a clip
A JPEG taken from the clip, to show where the clip itself is not wanted (lists, thumbnails). Redirects to the file in storage. Only video clips have one.
curl -L -o poster.jpg https://www.bana.video/api/assets/4b7e0c19d2a35f68/poster \
-H "Authorization: Bearer $API_TOKEN"
307Redirect to the picture.
404No such file, or it is not a clip.
/api/assets/{id}Delete an uploaded file
Removes the file, and the poster of a clip. A video that still uses it shows nothing in its place until you choose another.
curl -X DELETE https://www.bana.video/api/assets/9f2c1d7a40b3e865 \
-H "Authorization: Bearer $API_TOKEN"
200{ ok: true }.
404No such file.
Exports
An export turns a stored video into a file. Several can run for the same video, in different formats and sizes.
/api/videos/{id}/exportsStart an export
The body is the export options; every field is optional. An empty body exports a 1080p MP4 in the shape the video was made for (aspect of the document), or 16:9 when it names none.
{
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30
}
curl -X POST https://www.bana.video/api/videos/b921df158655611f/exports \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30
}'
202The job. Location points to it.
{
"job": {
"id": "75ae15a72b77b7b1",
"kind": "export",
"videoId": "b921df158655611f",
"status": "running",
"phase": "Drawing frames",
"progress": 0.42,
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30,
"quality": "standard",
"captions": true
},
"error": null,
"file": null,
"createdAt": "2026-10-12T21:38:44.157Z",
"startedAt": "2026-10-12T21:38:44.159Z",
"finishedAt": null
}
}
400An option is invalid (unknown field, out of range).
/api/videos/{id}/exportsList a video's exports, newest first
curl https://www.bana.video/api/videos/b921df158655611f/exports \
-H "Authorization: Bearer $API_TOKEN"
200The jobs.
{
"jobs": [
{
"id": "75ae15a72b77b7b1",
"kind": "export",
"videoId": "b921df158655611f",
"status": "running",
"phase": "Drawing frames",
"progress": 0.42,
"options": {
"format": "mp4",
"aspect": "9:16",
"resolution": "1080p",
"fps": 30,
"quality": "standard",
"captions": true
},
"error": null,
"file": null,
"createdAt": "2026-10-12T21:38:44.157Z",
"startedAt": "2026-10-12T21:38:44.159Z",
"finishedAt": null
}
]
}
/api/jobs/{id}Cancel a running job, or delete a finished one
A queued or running job is stopped and returned as cancelled. A finished job is removed together with its file.
curl -X DELETE https://www.bana.video/api/jobs/75ae15a72b77b7b1 \
-H "Authorization: Bearer $API_TOKEN"
200The cancelled job.
204A finished job was removed.
Public watch and embed
These need no API token. They only work for videos whose visibility is public; private and unknown ids both answer 404.
/api/oembedoEmbed 1.0 for a watch-page URL
Query: url (a /watch/{id} address on this site), optional maxwidth, maxheight, format=json.
curl "https://www.bana.video/api/oembed?url=https%3A%2F%2Fwww.bana.video%2Fwatch%2Fb921df158655611f&format=json"
200A video oEmbed object with an iframe html field.
404Not a public video on this site.
/api/public/videos/{id}A public video as a payload, for the web component
What <video-player src="…"> reads: the document, the timings and the address of the voice-over. Any origin may read it (CORS). See the web component guide.
curl "https://www.bana.video/api/public/videos/b921df158655611f"
200{ spec, timings, audioUrl }.
404Private or unknown.
/api/public/videos/{id}/audioNarration MP3 for a public video
curl -I "https://www.bana.video/api/public/videos/b921df158655611f/audio"
307Redirect to the audio when it is current.
404Private, unknown, or narration not ready.
/api/public/videos/{id}/assets/{assetId}A picture referenced by a public video
curl -I "https://www.bana.video/api/public/videos/b921df158655611f/assets/0123456789abcdef"
307Redirect to the file in storage.
404The video is not public, or the asset is not used in its document.
Reference data
Everything the engine accepts, as data, for building your own tools around it.
/api/schemaJSON Schema of the video document
curl https://www.bana.video/api/schema \
-H "Authorization: Bearer $API_TOKEN"
200JSON Schema, draft 2020-12.
/api/catalogScenes, palettes, fonts, backgrounds, domains and more
Each scene comes with a description, how its narration works and a complete example.
curl https://www.bana.video/api/catalog \
-H "Authorization: Bearer $API_TOKEN"
200The catalog.
/api/examplesReady-made documents
curl https://www.bana.video/api/examples \
-H "Authorization: Bearer $API_TOKEN"
200{ examples: [{ id, spec }] }.
/api/examples/{id}One ready-made video as a payload, with its voice-over
The same shape as a public video, for <video-player src="…">. Any origin may read it (CORS).
curl https://www.bana.video/api/examples/ads-videobana \
-H "Authorization: Bearer $API_TOKEN"
200{ spec, timings, audioUrl }.
404No example has this id.
/api/healthWhat works on this server
Checks ffmpeg, Chrome, the voice server, the AI key, the database and the file storage.
curl https://www.bana.video/api/health \
-H "Authorization: Bearer $API_TOKEN"
200status is ok or degraded; checks explains each part.
{
"status": "ok",
"checks": {
"ffmpeg": {
"ok": true,
"detail": "ffmpeg version 7.1"
},
"voice": {
"ok": true,
"detail": "Reachable"
}
}
}
New to the document format? Read the JSON format and the scene reference.