Send one video and the name of a look. Storias transcribes it, directs the film, sources or generates the visuals, and renders a finished, captioned film — over REST, or through MCP for an AI assistant.
{
"templates": [
{
"id": "newsroom",
"name": "Newsroom",
"tier": "pro",
"description": "Broadcast clarity: fast captions, decisive cuts and a clean lower third. The default when someone is talking to camera and the words carry the piece.",
"best_for": [
"talking-head updates",
"news commentary",
"announcements",
"quick explainers"
],
"avoid_for": [
"silent footage",
"product beauty shots",
"long-form documentary"
],
"aspect_ratios": [
"9:16"
],
"preview_url": "https://edit.storiasai.com/previews/newsroom.mp4"
},
{
"id": "scrapbook",
"name": "Scrapbook",
"tier": "pro",
"description": "Tactile layers, paper cutouts and handwritten notes around the speaker. Warm and personal rather than corporate.",
"best_for": [
"personal stories",
"travel diaries",
"behind the scenes",
"community updates"
],
"avoid_for": [
"financial reporting",
"formal corporate communication",
"technical documentation"
],
"aspect_ratios": [
"9:16"
],
"preview_url": "https://edit.storiasai.com/previews/scrapbook.mp4"
}
]
}
3 · Upload the clip
Ask for a one-time URL, then PUT the file to it with the same Content-Type you declared. Already have a link instead? Skip straight to step 4 with video_url.
Always send Idempotency-Key — a UUID you generate — so retrying this exact call after a timeout returns the same film instead of starting, and charging for, a second one.
Open Settings → API keys in the web app, signed in on an active plan, and create a key. The key is shown once — Storias stores only its hash, so a lost key is replaced, never recovered. Send it as Authorization: Bearer storias_sk_live_… on every call. Treat it like a payment instrument: it spends real credits, so keep it on a server and out of any code a browser downloads.
Reference
Endpoints
Every path this API serves, with its fields, straight from the live spec.
Templates
The looks a film can be made in, and what each one is for.
GET/v1/templates
List the looks a film can be made in
Call this before every first render and choose by best_for and avoid_for, not by name. The wrong look is the most common way an API-made film disappoints: a property tour in a financial-reporting look is technically a success and a waste of the customer's credits.
aspect_ratios is per template and is not advisory. A Pro look is drawn for 9:16 only, and asking for 16:9 on one is refused rather than silently corrected.
A Brand key also gets brand_allows on every look: the ones its Brand does not allow are refused.
Responses
Status
Meaning
200
Every look currently available.
401
The key is missing, malformed or revoked.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
200 response
{
"templates": [
{
"id": "newsroom",
"name": "Newsroom",
"tier": "pro",
"description": "Broadcast clarity: fast captions, decisive cuts and a clean lower third. The default when someone is talking to camera and the words carry the piece.",
"best_for": [
"talking-head updates",
"news commentary",
"announcements",
"quick explainers"
],
"avoid_for": [
"silent footage",
"product beauty shots",
"long-form documentary"
],
"aspect_ratios": [
"9:16"
],
"preview_url": "https://edit.storiasai.com/previews/newsroom.mp4"
},
{
"id": "scrapbook",
"name": "Scrapbook",
"tier": "pro",
"description": "Tactile layers, paper cutouts and handwritten notes around the speaker. Warm and personal rather than corporate.",
"best_for": [
"personal stories",
"travel diaries",
"behind the scenes",
"community updates"
],
"avoid_for": [
"financial reporting",
"formal corporate communication",
"technical documentation"
],
"aspect_ratios": [
"9:16"
],
"preview_url": "https://edit.storiasai.com/previews/scrapbook.mp4"
}
]
}
Renders
Making a film and following it to completion.
POST/v1/uploads
Get a URL to upload the source video to
Returns a one-time URL. PUT the video bytes to it with the same Content-Type you declared, then pass the returned asset_id to POST /v1/renders.
The video should be one person speaking to camera, with clear audio — that is what every look is built around. Up to 60 minutes and 2 GB.
Request body
Field
Type
Values
Description
content_typerequired
string
video/mp4 · video/quicktime · video/x-m4v
The video's real type. Declaring one type and sending another is refused later, before anything is charged.
{
"content_type": "video/mp4"
}
Responses
Status
Meaning
201
Upload here, then render.
401
The key is missing, malformed or revoked.
415
That content_type is not a video we can read.
429
Too many requests on this key. retry_after says how long to wait.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
Use this when nobody kept a render_id: the account's most recent renders, newest first, including any still being made. It is every render the account made, whichever way it was started.
Each entry says what the film is, when it began and how it ended. A completed entry carries fresh output links (both signed and short-lived, like GET /v1/renders/{render_id}'s); a failed entry carries its error.reference. A live progress is not in the list: read GET /v1/renders/{render_id} for the film somebody is watching.
A film is listed for about two days after it finishes (the window GET /v1/renders/{render_id} answers in too), and then it is gone from here. Download the file rather than relying on this list to find it later.
A page holds 10 entries unless limit says otherwise, up to 20. When next_cursor is not null, pass it back as cursor for the next page. A key made for a Brand does not list renders; it reads its company's films with GET /v1/films/{film_id}.
Parameters
Parameter
In
Type
Description
limit
query
integer
How many renders to return.
cursor
query
string
The next_cursor of the previous page. Opaque: pass it back exactly as it was given.
Responses
Status
Meaning
200
The account's renders, newest first.
400
limit or cursor cannot be used — see code.
401
The key is missing, malformed or revoked.
403
This key cannot read renders, or it is a Brand key, which reads its films with GET /v1/films/{film_id}.
429
Too many requests on this key. retry_after says how long to wait.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
Starts a render and returns immediately with a render_id. The film is not ready; poll GET /v1/renders/{render_id}.
Pass eithervideo_url (we fetch it — no upload step) orasset_id from POST /v1/uploads.
Credits are spent here. One per second of finished film, minimum 15, held when the render is admitted. A refusal for INSUFFICIENT_CREDITS tells you exactly how many are missing.
Always send Idempotency-Key. It is the render's own id, so the same key returns the same render however many times you retry — without it, a retried request is a second film and a second charge. The length is measured from the file you uploaded, never from anything you send.
With a Brand key the film is filed to the Brand's library the moment it begins, under the render's id (so render_id is also its film_id), and is made in the Brand's own style. A look the Brand does not allow is refused with FORBIDDEN and allowed_templates. title names the film in the library.
A caption style (a template_id that starts with caption-) takes options: clip art, and the colours of its words. Any other look is refused them.
Parameters
Parameter
In
Type
Description
Idempotency-Key
header
string
A UUID you generate. Reuse it on every retry of this render; never reuse it for a different one.
Request body
Exactly one of: asset_id or video_url.
Field
Type
Values
Description
asset_id
string
—
From POST /v1/uploads, after the bytes are uploaded.
video_url
string (uri)
—
An https link we fetch ourselves — use this instead of asset_id and skip the upload step entirely. It must serve video/mp4, video/quicktime or video/x-m4v, be reachable without credentials, and stay valid for a couple of minutes. Up to 2 GB.
A look from GET /v1/templates. Choose it by best_for and avoid_for.
aspect_ratio
string
9:16 · 16:9
16:9 needs an Ultra template. A Pro look is refused rather than quietly turned vertical.
title
string
—
Brand keys only: the film's name in the company's library, up to 120 characters. A key with no Brand ignores it.
options
object
—
Caption styles only (a template_id that starts with caption-): clip art, and the colours of the words. With any other look the request is refused with INVALID_REQUEST rather than quietly ignored, and so is any name not listed here. Leave it out for the style's own look.
options.clip_art
boolean
—
Add clip art: a few small drawings of what is said and shown, like a pan for the kitchen or a tag for the price. Off unless true. The film costs the same either way.
options.colors
object
—
Any of the style's three colours, each written #RRGGBB; one left out keeps the style's own. A colour too close to the plate it sits on is corrected, so the words stay readable.
options.colors.text
string
—
The words.
options.colors.accent
string
—
The key words, figures and prices, and the style's own marks: its rules, ticks and pen.
options.colors.plate
string
—
What the words sit on, in the styles that have one: Scale's band, Frame's plates, Sticker Drop's stickers. A style with none ignores it.
Poll about every ten seconds. status is queued, processing, completed or failed, and only the last two are final.
A Brand key sees only the renders that are films of its Brand, and handing it a completed film's links is a download from the company's library: in its audit log, at most once an hour for the same key and film.
On completed you get video_url and download_url. Both are signed and expire — download the file, do not store the link. On failed you get a short message and a reference: a word and four characters that support can use to find the exact film, which is what to show a person.
Parameters
Parameter
In
Type
Description
render_idrequired
path
string
From POST /v1/renders.
Responses
Status
Meaning
200
The render's state. A render belonging to another account is not found, never forbidden.
401
The key is missing, malformed or revoked.
404
No render with that id on this account.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
Check credits_remaining before submitting a render you cannot pay for. One key belongs to one user, and spends that user's credits. A key made for a company's Brand also says which Brand, in brand.
Responses
Status
Meaning
200
The account behind this key.
401
The key is missing, malformed or revoked.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
A company's library, read with a key made for one of its Brands.
GET/v1/films/{film_id}
A film in your Brand's library, and the film itself
Read with a key made for one of a company's Brands, for any film of that Brand, whoever in the company made it. This is where a film.ready webhook points. A key with no Brand, another Brand's film, and a film that is not in a library all answer 404.
On completed you get output.video_url and output.download_url. Both are signed and expire. Handing them out is a download, and every download from a company's library is in its audit log: this one records it at most once an hour for the same key and film.
Parameters
Parameter
In
Type
Description
film_idrequired
path
string
From a film.ready webhook, or the render_id of a render a Brand key started.
Responses
Status
Meaning
200
The film.
401
The key is missing, malformed or revoked, or it is a Brand key that no longer works.
404
No film with that id in this key's Brand.
500
Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
pro is captioned and speaker-led, 9:16 only. ultra is a directed film and works in either canvas. captions keeps the recording whole and turns every word into motion design, 9:16 only, made for real estate agents.
descriptionrequired
string
—
What kind of film this look makes.
best_forrequired
array of string
—
Subjects this look was built for.
avoid_forrequired
array of string
—
Where it will disappoint. Read this half — it is what stops a bad choice.
aspect_ratiosrequired
array of string
9:16 · 16:9
The canvases this look really renders. Asking for another is refused, not corrected.
preview_url
string (uri)
—
A short example film in this look, 9:16. A caption style has none yet.
tags
array of string
—
Captions only: what the style is for and how it feels, "Real Estate" first.
preview_url_16x9
string (uri)
—
The wide cut. Ultra looks only — a Pro look has no 16:9 version because it does not render one.
brand_allows
boolean
—
Brand keys only: whether this key's Brand allows the look. A film in a look it does not allow is refused.
Account
Field
Type
Values
Description
user_idrequired
string (uuid)
—
—
planrequired
string
—
The account's current plan, or free.
credits_remainingrequired
integer
—
Spendable credits. One credit is one second of finished film; every render costs at least 15.
permissionsrequired
object
—
—
permissions.render
boolean
—
This key may start renders.
permissions.read
boolean
—
This key may read templates, renders and this account.
brand
object
—
Brand keys only: the company Brand this key makes films for. Its films are filed to it, in its looks only.
brand.idrequired
string (uuid)
—
—
brand.namerequired
string
—
—
brand.looksrequired
array or null
—
The looks the Brand allows, by template_id; null is every look.
brand.own_looks
array of object
—
The company's own looks this Brand allows, each with the most a second of it can cost. Present while the company's own looks make films.
Upload
Field
Type
Values
Description
asset_idrequired
string
—
Pass this to POST /v1/renders once the PUT has succeeded.
upload_urlrequired
string (uri)
—
One-time URL. PUT the bytes here.
methodrequired
string
PUT
—
headers
object
—
Send these with the PUT.
expires_inrequired
integer
—
Seconds this URL is valid for.
RenderRequest
Give the video one of two ways: asset_id if you uploaded it, or video_url if you have a link.
Exactly one of: asset_id or video_url.
Field
Type
Values
Description
asset_id
string
—
From POST /v1/uploads, after the bytes are uploaded.
video_url
string (uri)
—
An https link we fetch ourselves — use this instead of asset_id and skip the upload step entirely. It must serve video/mp4, video/quicktime or video/x-m4v, be reachable without credentials, and stay valid for a couple of minutes. Up to 2 GB.
A look from GET /v1/templates. Choose it by best_for and avoid_for.
aspect_ratio
string
9:16 · 16:9
16:9 needs an Ultra template. A Pro look is refused rather than quietly turned vertical.
title
string
—
Brand keys only: the film's name in the company's library, up to 120 characters. A key with no Brand ignores it.
options
object
—
Caption styles only (a template_id that starts with caption-): clip art, and the colours of the words. With any other look the request is refused with INVALID_REQUEST rather than quietly ignored, and so is any name not listed here. Leave it out for the style's own look.
options.clip_art
boolean
—
Add clip art: a few small drawings of what is said and shown, like a pan for the kitchen or a tag for the price. Off unless true. The film costs the same either way.
options.colors
object
—
Any of the style's three colours, each written #RRGGBB; one left out keeps the style's own. A colour too close to the plate it sits on is corrected, so the words stay readable.
options.colors.text
string
—
The words.
options.colors.accent
string
—
The key words, figures and prices, and the style's own marks: its rules, ticks and pen.
options.colors.plate
string
—
What the words sit on, in the styles that have one: Scale's band, Frame's plates, Sticker Drop's stickers. A style with none ignores it.
Render
Field
Type
Values
Description
render_idrequired
string (uuid)
—
—
statusrequired
string
queued · processing · completed · failed
Only completed and failed are final.
progressrequired
integer
—
Never reaches 100 before completed, so do not treat 99 as done.
output
object
—
On completed only. This is where the film is — not at the top level.
output.video_urlrequired
string or null (uri)
—
Signed, for playback. Expires.
output.download_urlrequired
string or null (uri)
—
Signed, with a filename attached, for saving. Expires.
output.expires_inrequired
integer
—
Seconds until both URLs stop working (7200). Download the file rather than storing the link.
output.duration_seconds
integer or null
—
The finished film's length.
output.aspect_ratio
string
9:16 · 16:9
—
credits_used
integer or null
—
On completed: what this film actually cost.
template_id
string
—
On completed: the look it was rendered in.
error
object
—
On failed only.
error.coderequired
string
RENDER_FAILED
—
error.messagerequired
string
—
What to show a person. Deliberately carries no internal detail.
error.referencerequired
string
—
A word and four characters, e.g. Green 7F3A. Support finds the film by this.
A stable machine code. Branch on this, never on message — the wording can improve, the code cannot change.
error.messagerequired
string
—
One sentence for a person reading a log.
error.required
integer
—
On INSUFFICIENT_CREDITS: credits this render needs.
error.available
integer
—
On INSUFFICIENT_CREDITS: credits the account holds.
error.missing
integer
—
On INSUFFICIENT_CREDITS: how many short. Buy at least this many.
error.retry_after
integer
—
On RATE_LIMITED and TOO_MANY_RENDERS: seconds to wait before retrying. Sent as the Retry-After header too.
error.limit_bytes
integer
—
On FILE_TOO_LARGE: the largest source this API accepts.
error.bytes
integer
—
On FILE_TOO_LARGE: how big the source actually is.
error.aspect_ratios
array of string
—
On INVALID_ASPECT_RATIO: the ratios this template does support.
error.allowed_templates
array of string
—
On FORBIDDEN for a Brand key: the looks its Brand allows. Choose one of these instead.
Renders
Statuses and polling
A render's status is one of queued, processing, completed, failed. Only completed and failed are final; poll GET /v1/renders/{render_id} about every ten seconds until you see one of them, and never treat a progress of 99 as done. On completed, output.video_url and output.download_url are signed and expire in 2 hours — download the file rather than storing the link. On failed, error.reference is a word and four characters (for example Green 7F3A): show it to whoever hit the failure, since it is what support uses to find the exact film. A failed film is never charged.
Billing
Credits
One credit buys one second of finished film, with a minimum of 15 credits per render. The cost is held on the account when the render is admitted, and charged for real only on delivery. A render that fails instead is never charged, and its hold is released.
Limits
Plans and rate limits
Plan ceilings come from the same contract the apps enforce; a key inherits its account’s plan.
Starter
Films up to 3 minutes long
3 renders at once
Standard queue
Creator
Films up to 6 minutes long
6 renders at once
Standard queue
Pro
Films up to 10 minutes long
10 renders at once
Priority rendering
Limit
Value
Render calls
POST /v1/renders— 20 per minute, per key
Read calls
Everything else— 120 per minute, per key
Source file size
2 GB
Source length
Up to 60 minutes (a plan's own film-length limit above may be shorter)
A rate-limited or too-many-renders refusal carries retry_after seconds in the body and in the Retry-After header. It is never charged.
When it says no
Errors
Branch on code, never on message — the wording can improve, the code cannot change. Every response, success or failure, carries an X-Request-Id header.
Code
HTTP status
What to do
INVALID_API_KEY
401
Check the key was copied in full and has not been revoked in Settings → API keys. Mint a new one if in doubt.
FORBIDDEN
403
This key is not allowed to do that — for example a read-only key used on a write call. Use a key with the right scope.
NOT_FOUND
404
Nothing on this account matches that id or path. A render that belongs to a different account also answers this, never 403, so an id cannot be used to probe someone else’s films.
INVALID_REQUEST
400
Something in the request body could not be used. Check field names, types and required fields against this page.
INVALID_TEMPLATE
400
That template_id does not exist. Call GET /v1/templates for the current list rather than hard-coding one.
INVALID_ASPECT_RATIO
400
That template does not render this aspect ratio. aspect_ratios on the error, and on each template from GET /v1/templates, says which ones it does — a Pro look is 9:16 only.
INVALID_ASSET
422
The asset_id is not in storage, or is not readable as a video. Confirm the PUT to upload_url finished before calling POST /v1/renders.
UNSUPPORTED_VIDEO
415
The file is not a video type this API reads. Use video/mp4, video/quicktime or video/x-m4v, and declare the real type.
FILE_TOO_LARGE
413
limit_bytes and bytes on the error say the cap and the actual size. Compress or trim the source and try again.
CLIP_TOO_LONG
413
The clip is longer than this product renders. Trim it and try again.
INSUFFICIENT_CREDITS
402
required, available and missing say exactly how many credits are short. Nothing was charged; buy at least missing more.
RATE_LIMITED
429
Too many requests on this key. Wait retry_after seconds — also sent as the Retry-After header — and retry.
TOO_MANY_RENDERS
429
Too many renders already in flight on this account. Wait retry_after seconds and retry; nothing was charged.
RENDER_FAILED
500
The render did not finish. message and reference from GET /v1/renders/{render_id} are what to show a person and what support needs to find it — never retry automatically, a retry is another charge.
RENDERING_PAUSED
503
Rendering is paused for everyone for a moment. Nothing was charged; wait and retry later.
INTERNAL
500
Something went wrong on our side. The X-Request-Id response header is what support needs to find this exact call.
The error envelope
Every refusal, on every endpoint, is this same shape.
A stable machine code. Branch on this, never on message — the wording can improve, the code cannot change.
error.messagerequired
string
—
One sentence for a person reading a log.
error.required
integer
—
On INSUFFICIENT_CREDITS: credits this render needs.
error.available
integer
—
On INSUFFICIENT_CREDITS: credits the account holds.
error.missing
integer
—
On INSUFFICIENT_CREDITS: how many short. Buy at least this many.
error.retry_after
integer
—
On RATE_LIMITED and TOO_MANY_RENDERS: seconds to wait before retrying. Sent as the Retry-After header too.
error.limit_bytes
integer
—
On FILE_TOO_LARGE: the largest source this API accepts.
error.bytes
integer
—
On FILE_TOO_LARGE: how big the source actually is.
error.aspect_ratios
array of string
—
On INVALID_ASPECT_RATIO: the ratios this template does support.
error.allowed_templates
array of string
—
On FORBIDDEN for a Brand key: the looks its Brand allows. Choose one of these instead.
AI assistants
Connect over MCP
Claude, ChatGPT and most agent frameworks can use Storias as five tools over one JSON-RPC (MCP) endpoint, authenticated with the same key as the REST API:
POST https://swpjvbgpnqbprqtbaksk.supabase.co/functions/v1/mcp
Authorization: Bearer $STORIAS_API_KEY
Content-Type: application/json
Send the standard MCP handshake (initialize, then tools/list or tools/call) as JSON-RPC 2.0 request bodies. In Claude Desktop or Claude Code, add it as a custom connector with that URL and an Authorization header carrying the same bearer key described above; any MCP-compatible client works the same way.
Tool
What it does
Parameters
list_looks
The looks a film can be made in, each with what it is for and what it is wrong for.
—
make_film
Turn one recording into a finished, directed, captioned film. Spends credits; returns an id, not a video.
video_url— An https link to the video file. Fetched immediately, so it must still be valid when this is called. Give this or `video`, not both.
video— A video attached in a chat assistant, as `{ download_url, file_id }` and optionally `mime_type` and `file_name`. Give this or `video_url`, not both.
lookrequired— The `id` of a look from `list_looks`.
aspect_ratio— `9:16` (default) or `16:9`. `16:9` needs a look whose `aspect_ratios` include it.
check_film
Where a film has got to, and the links to watch and download it once it is done.
render_idrequired— The id `make_film` or `list_films` returned.
check_credits
How many credits the account has left.
—
list_films
The account's most recent films, newest first, with fresh links for the finished ones. A film is listed for about two days after it finishes.
limit— How many films, 1 to 20. Default 10.
cursor— The `next_cursor` of the previous page.
A tool's failure comes back as a normal result with isError set, carrying a sentence the model can act on (for example, exactly how many credits are missing) — not a protocol error. make_film is safe to retry: calling it again with the same video and look within the hour returns the film already being made rather than starting a second one; after that hour the same call starts, and charges for, a new film. A film that failed was not charged and can be started again. A finished film is final: there is no edit or re-cut.
00:00:00:00 · New project
Build the next integration.
The full OpenAPI document is published at https://api.storiasai.com/v1/openapi.json, no key required — read it straight into any client generator.