Videobana

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:

  1. 1
    Send the document
    With POST /api/render, or POST /api/videos to store it first. It is validated; problems come back as a 422 with their paths.
  2. 2
    Start the export
    Unless you used render, POST /api/videos/{id}/exports with the format, shape and resolution you want. You get a job back immediately (202).
  3. 3
    Wait for the job
    Poll GET /api/jobs/{id} every second or two until status is done. The first export of a video also records the voice-over, which takes a little longer.
  4. 4
    Download
    GET /api/jobs/{id}/download returns the file.
Exports run in the background, a few at a time. Anything over the limit waits its turn and shows as 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": []
    }
  }
}
StatuscodeWhen
400bad_requestThe request body is missing, too large (over 1 MB), not JSON, or has fields the endpoint does not know.
401unauthorizedThe request carried no API token, or one that is not valid (unknown, revoked or expired).
402quota_exceededThe 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).
404not_foundNo such video or job.
409conflictThe 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.
422invalid_specThe video document has problems. error.details.errors lists each one with its path and a hint.
429too_many_requestsYou already have as many exports (or voice-over recordings) in progress as are allowed at once. Wait for one to finish, or cancel it.
502upstream_errorThe AI model did not produce a valid document, even after repairs. error.details.errors has the last problems.
503service_unavailableA 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.

FieldTypeWhat it does
formatenum

What to produce.

mp4webmmovgifmp3pngsrtvttjson

aspectenum

Shape of the picture: 16:9 landscape, 9:16 vertical, 1:1 square... Default: the shape the video was made for (aspect in its document), else 16:9.

16:94:31:14:53:49:16

resolutionenum

Pixels of the shorter side: 1080p is 1920×1080 for 16:9 and 1080×1920 for 9:16.

360p480p720p1080p1440p2160p

fpsinteger

Frames per second.

Limits: 10–60

qualityenum

Encoder quality. draft is fastest.

draftstandardhigh

captionsboolean

Draw the captions into the picture. Default true.

atnumber

For png: the moment (seconds) to capture. Default: the title card.

settleinteger

For png: milliseconds of animation played before the capture. Default 2000, when everything has finished arriving; 0 freezes the first instant of every animation.

Limits: 0–10000

musicobject

Mix background music under the narration (mp4, webm, mov and mp3).

└ srcrequiredstring

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

└ volumenumber

Loudness of the music, 0 to 1, where the narration is 1. Default 0.18.

Limits: 0–1

└ duckboolean

Lower the music further while the narrator speaks. Default true.

callbackUrlstring

When the job finishes (done, failed or cancelled), the job is POSTed to this address as JSON: { "event": "job.finished", "job": { … } }.

Limits: up to 2000 characters

previewSecondsnumber

Render only the first N seconds, to check settings quickly.

Limits: 1–3600

formatProducesContent typeUses
mp4H.264 + AAC. Plays everywhere: social media, phones, slides.video/mp4Voice-over and picture
webmVP9 + Opus. Smaller files, ideal for the web.video/webmVoice-over and picture
movH.264 + AAC in a QuickTime container, for Final Cut and iMovie.video/quicktimeVoice-over and picture
gifSilent loop, limited to 480p and 15 fps.image/gifVoice-over and picture
mp3Just the narration, for podcasts.audio/mpegThe voice-over
pngOne frame, for thumbnails (set `at`).image/pngVoice-over and picture
srtCaptions timed to the narration.application/x-subripThe script only
vttCaptions for HTML5 video players.text/vttThe script only
jsonThe spec this video was made from.application/jsonThe script only
  • resolution names the pixels of the shorter side. 1080p with 9:16 is 1080×1920.
  • gif is capped at 480p and 15 fps and has no sound. png captures one frame, at at seconds.
  • previewSeconds renders only the start of the video: a quick way to check a setting before the full export.
  • music mixes background music into mp4, webm, mov and mp3: { "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 with duck gets quieter whenever the narrator speaks. Without it, an export uses the music setting of the video itself, the one the editor writes. A video with sound (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.
  • callbackUrl saves 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.

POST/api/render

Create a video and start exporting it

options is optional and takes everything listed under Export options. The voice-over is recorded first when needed.

Request body (JSON)
{
  "spec": "{ … the document … }",
  "options": {
    "format": "mp4",
    "aspect": "9:16",
    "resolution": "1080p",
    "fps": 30
  }
}
Try it
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
    }
  }'
Answers

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.

GET/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.

Try it
curl https://www.bana.video/api/jobs/75ae15a72b77b7b1 \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

GET/api/jobs/{id}/download

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

Try it
curl -OJL https://www.bana.video/api/jobs/75ae15a72b77b7b1/download \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

POST/api/sources

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

Request body (JSON)
{
  "url": "https://en.wikipedia.org/wiki/Photosynthesis"
}
Try it
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"
  }'
Answers

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.

POST/api/generate

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

Request body (JSON)
{
  "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
}
Try it
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
  }'
Answers

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.

POST/api/revise

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

Request body (JSON)
{
  "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
    }
  ]
}
Try it
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"
  }'
Answers

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.

POST/api/publish

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

Request body (JSON)
{
  "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"
  ]
}
Try it
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"
    ]
  }'
Answers

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.

POST/api/music

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

Request body (JSON)
{
  "mood": "uplifting",
  "description": "with a light acoustic guitar",
  "length": "clip"
}
Try it
curl -X POST https://www.bana.video/api/music \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mood": "uplifting",
    "length": "clip"
  }'
Answers

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.

POST/api/images

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

Request body (JSON)
{
  "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"
}
Try it
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"
  }'
Answers

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.

POST/api/images/plan

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

Request body (JSON)
{
  "spec": "{ … the document … }",
  "style": "auto",
  "amount": "balanced"
}
Try it
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"
  }'
Answers

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.

GET/api/assets

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

Try it
curl https://www.bana.video/api/assets?kind=image&limit=20 \
  -H "Authorization: Bearer $API_TOKEN"
Answers

200{ assets: [ … ] }, in the shape of an uploaded file.

GET/api/usage

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

Try it
curl https://www.bana.video/api/usage \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

POST/api/product/crawl

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

Request body (JSON)
{
  "url": "https://aurora.audio/one"
}
Try it
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"
  }'
Answers

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.

POST/api/product/pictures

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

Request body (JSON)
{
  "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
}
Try it
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"
    ]
  }'
Answers

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

POST/api/product/compose

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

Request body (JSON)
{
  "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
}
Try it
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"
  }'
Answers

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.

POST/api/validate

Check a document without storing it

Returns every error and warning with its path and often a hint, plus stats for a valid document.

Request body (JSON)
{
  "spec": "{ … the document … }"
}
Try it
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."
        }
      ]
    }
  }'
Answers

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
  }
}
POST/api/videos

Store a video

Request body (JSON)
{
  "spec": "{ … the document … }"
}
Try it
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."
        }
      ]
    }
  }'
Answers

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.

GET/api/videos

List videos, newest first

Try it
curl https://www.bana.video/api/videos \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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"
    }
  ]
}
GET/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.

Try it
curl https://www.bana.video/api/videos/b921df158655611f \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

PUT/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.

Request body (JSON)
{
  "spec": "{ … the new document … }"
}
Try it
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."
        }
      ]
    }
  }'
Answers

200The updated video.

422The document has problems.

PUT/api/videos/{id}/visibility

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

Request body (JSON)
{
  "visibility": "public"
}
Try it
curl -X PUT https://www.bana.video/api/videos/b921df158655611f/visibility \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "visibility": "public"
  }'
Answers

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.

POST/api/videos/{id}/duplicate

Copy a video

The copy has a new id and the title … (copy). Change it freely: the original is not touched.

Try it
curl -X POST https://www.bana.video/api/videos/b921df158655611f/duplicate \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

DELETE/api/videos/{id}

Delete a video and all of its exports

Try it
curl -X DELETE https://www.bana.video/api/videos/b921df158655611f \
  -H "Authorization: Bearer $API_TOKEN"
Answers

204Deleted.

POST/api/videos/{id}/narration

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

Try it
curl -X POST https://www.bana.video/api/videos/b921df158655611f/narration \
  -H "Authorization: Bearer $API_TOKEN"
Answers

202The job to follow with GET /api/jobs/{id}.

503The voice server is not reachable.

GET/api/videos/{id}/audio

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

Try it
curl -L -o narration.mp3 https://www.bana.video/api/videos/b921df158655611f/audio \
  -H "Authorization: Bearer $API_TOKEN"
Answers

307Redirect to the audio (audio/mpeg).

404Not recorded, or out of date.

POST/api/narration/draft

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

Request body (JSON)
{
  "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."
      }
    ]
  }
}
Try it
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."
        }
      ]
    }
  }'
Answers

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.

POST/api/narration/draft/lookup

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

Request body (JSON)
{
  "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."
      }
    ]
  }
}
Try it
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."
        }
      ]
    }
  }'
Answers

200{ draft: { fingerprint, timings, audioUrl } }, or { draft: null } when there is none.

422The video document has problems.

GET/api/narration/draft/{fingerprint}/audio

A recorded draft voice-over as MP3

Redirects to the recording, which supports Range requests. The audioUrl of a draft points here.

Try it
curl -L -o draft.mp3 https://www.bana.video/api/narration/draft/3cc982906520/audio \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

POST/api/assets

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

Try it
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
Answers

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

POST/api/assets/uploads

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

Request body (JSON)
{
  "kind": "audio",
  "bytes": 482113
}
Try it
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
  }'
Answers

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.

POST/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.

Request body (JSON)
{
  "kind": "audio",
  "name": "voice.m4a"
}
Try it
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"
  }'
Answers

201The file, as in the answer of the upload above.

400The file did not arrive, or is not what kind says.

GET/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.

Try it
curl -L -o voice.mp3 https://www.bana.video/api/assets/9f2c1d7a40b3e865 \
  -H "Authorization: Bearer $API_TOKEN"
Answers

307Redirect to the file.

404No such file.

GET/api/assets/{id}/poster

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

Try it
curl -L -o poster.jpg https://www.bana.video/api/assets/4b7e0c19d2a35f68/poster \
  -H "Authorization: Bearer $API_TOKEN"
Answers

307Redirect to the picture.

404No such file, or it is not a clip.

DELETE/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.

Try it
curl -X DELETE https://www.bana.video/api/assets/9f2c1d7a40b3e865 \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

POST/api/videos/{id}/exports

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

Request body (JSON)
{
  "format": "mp4",
  "aspect": "9:16",
  "resolution": "1080p",
  "fps": 30
}
Try it
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
  }'
Answers

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

GET/api/videos/{id}/exports

List a video's exports, newest first

Try it
curl https://www.bana.video/api/videos/b921df158655611f/exports \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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
    }
  ]
}
DELETE/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.

Try it
curl -X DELETE https://www.bana.video/api/jobs/75ae15a72b77b7b1 \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.

GET/api/oembed

oEmbed 1.0 for a watch-page URL

Query: url (a /watch/{id} address on this site), optional maxwidth, maxheight, format=json.

Try it
curl "https://www.bana.video/api/oembed?url=https%3A%2F%2Fwww.bana.video%2Fwatch%2Fb921df158655611f&format=json"
Answers

200A video oEmbed object with an iframe html field.

404Not a public video on this site.

GET/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.

Try it
curl "https://www.bana.video/api/public/videos/b921df158655611f"
Answers

200{ spec, timings, audioUrl }.

404Private or unknown.

GET/api/public/videos/{id}/audio

Narration MP3 for a public video

Try it
curl -I "https://www.bana.video/api/public/videos/b921df158655611f/audio"
Answers

307Redirect to the audio when it is current.

404Private, unknown, or narration not ready.

GET/api/public/videos/{id}/assets/{assetId}

A picture referenced by a public video

Try it
curl -I "https://www.bana.video/api/public/videos/b921df158655611f/assets/0123456789abcdef"
Answers

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.

GET/api/schema

JSON Schema of the video document

Try it
curl https://www.bana.video/api/schema \
  -H "Authorization: Bearer $API_TOKEN"
Answers

200JSON Schema, draft 2020-12.

GET/api/catalog

Scenes, palettes, fonts, backgrounds, domains and more

Each scene comes with a description, how its narration works and a complete example.

Try it
curl https://www.bana.video/api/catalog \
  -H "Authorization: Bearer $API_TOKEN"
Answers

200The catalog.

GET/api/examples

Ready-made documents

Try it
curl https://www.bana.video/api/examples \
  -H "Authorization: Bearer $API_TOKEN"
Answers

200{ examples: [{ id, spec }] }.

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

Try it
curl https://www.bana.video/api/examples/ads-videobana \
  -H "Authorization: Bearer $API_TOKEN"
Answers

200{ spec, timings, audioUrl }.

404No example has this id.

GET/api/health

What works on this server

Checks ffmpeg, Chrome, the voice server, the AI key, the database and the file storage.

Try it
curl https://www.bana.video/api/health \
  -H "Authorization: Bearer $API_TOKEN"
Answers

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.