Developers

The Storias API

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.

  • Base URL api.storiasai.com
  • 1 credit = 1 second of finished film, 15 minimum
  • REST and MCP, same key

Quick start

Five calls to a finished film

Set STORIAS_API_KEY once and run these in order.

  1. 1 · Who am I, and what can I spend?

    Every call is a bearer key, so start by reading it back — a good way to prove the key works before anything spends credits.

    curl https://api.storiasai.com/v1/me \
      -H "Authorization: Bearer $STORIAS_API_KEY"
    {
      "user_id": "a059c6f4-604d-4f68-81f6-f839b9c95b0a",
      "plan": "weekly",
      "credits_remaining": 281,
      "permissions": {
        "render": true,
        "read": true
      }
    }
  2. 2 · Choose a look

    Pick a template by best_for and avoid_for, never by name — the wrong look is the most common way an API-made film disappoints.

    curl https://api.storiasai.com/v1/templates \
      -H "Authorization: Bearer $STORIAS_API_KEY"
    {
      "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. 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.

    curl https://api.storiasai.com/v1/uploads \
      -H "Authorization: Bearer $STORIAS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"content_type":"video/mp4"}'
    {
      "asset_id": "a059c6f4-604d-4f68-81f6-f839b9c95b0a/7afc924f-a906-4b3f-928c-5a36bed8a6fe.mp4",
      "upload_url": "https://swpjvbgpnqbprqtbaksk.supabase.co/storage/v1/object/upload/sign/uploads/example.mp4?token=example",
      "method": "PUT",
      "headers": {
        "content-type": "video/mp4"
      },
      "expires_in": 7200
    }
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: video/mp4" \
      --data-binary @clip.mp4
  4. 4 · Start the render

    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.

    curl https://api.storiasai.com/v1/renders \
      -H "Authorization: Bearer $STORIAS_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{"video_url":"https://example.com/talk.mp4","template_id":"newsroom","aspect_ratio":"9:16"}'
    {
      "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
      "status": "queued"
    }
  5. 5 · Poll for the finished film

    Check back about every ten seconds until status is completed or failed — both are final, and progress never reaches 100 before completed.

    curl https://api.storiasai.com/v1/renders/3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10 \
      -H "Authorization: Bearer $STORIAS_API_KEY"
    {
      "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
      "status": "completed",
      "progress": 100,
      "output": {
        "video_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?X-Amz-Signature=example",
        "download_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?response-content-disposition=attachment&X-Amz-Signature=example",
        "expires_in": 7200,
        "duration_seconds": 20,
        "aspect_ratio": "9:16"
      },
      "credits_used": 20,
      "template_id": "newsroom"
    }

Authentication

Getting a key

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
StatusMeaning
200Every look currently available.
401The key is missing, malformed or revoked.
500Something 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
FieldTypeValuesDescription
content_type required 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
StatusMeaning
201Upload here, then render.
401The key is missing, malformed or revoked.
415That content_type is not a video we can read.
429Too many requests on this key. retry_after says how long to wait.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
201 response
{
  "asset_id": "a059c6f4-604d-4f68-81f6-f839b9c95b0a/7afc924f-a906-4b3f-928c-5a36bed8a6fe.mp4",
  "upload_url": "https://swpjvbgpnqbprqtbaksk.supabase.co/storage/v1/object/upload/sign/uploads/example.mp4?token=example",
  "method": "PUT",
  "headers": {
    "content-type": "video/mp4"
  },
  "expires_in": 7200
}
GET/v1/renders

List this account's recent films

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
ParameterInTypeDescription
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
StatusMeaning
200The account's renders, newest first.
400limit or cursor cannot be used — see code.
401The key is missing, malformed or revoked.
403This key cannot read renders, or it is a Brand key, which reads its films with GET /v1/films/{film_id}.
429Too many requests on this key. retry_after says how long to wait.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
200 response
{
  "renders": [
    {
      "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
      "status": "completed",
      "template_id": "newsroom",
      "aspect_ratio": "9:16",
      "duration_seconds": 20,
      "credits_used": 20,
      "created_at": "2026-09-29T14:00:11.000Z",
      "finished_at": "2026-09-29T14:03:02.000Z",
      "output": {
        "video_url": "https://example.r2.cloudflarestorage.com/storias-films/films/example.mp4?X-Amz-Signature=example",
        "download_url": "https://example.r2.cloudflarestorage.com/storias-films/films/example.mp4?response-content-disposition=attachment&X-Amz-Signature=example",
        "expires_in": 7200
      }
    },
    {
      "render_id": "9d0e5c2a-4b8f-4e21-8a63-1f7c3b5d9e02",
      "status": "processing",
      "template_id": "scrapbook",
      "aspect_ratio": "9:16",
      "duration_seconds": 35,
      "credits_used": null,
      "created_at": "2026-09-29T14:10:40.000Z",
      "finished_at": null
    }
  ],
  "next_cursor": null
}
POST/v1/renders

Make a film

Starts a render and returns immediately with a render_id. The film is not ready; poll GET /v1/renders/{render_id}.

Pass either video_url (we fetch it — no upload step) or asset_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
ParameterInTypeDescription
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.

FieldTypeValuesDescription
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.
template_id required string newsroom · scrapbook · editorial · broadsheet · blueprint · dossier · briefing · prism · ultraEditorialPop · ultraFinancialJournal · ultraBusinessDocumentary · ultraInvestigativeDossier · ultraEvidence · ultraMindmap · ultraStoryboard · ultraMap · ultraData · ultraMindnet · ultraCountdown · ultraPlaybook · caption-kinetic · caption-scale · caption-cover · caption-floorplan · caption-marker · caption-stickers · caption-arc · caption-frame 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.
From a link — two calls in total
{
  "video_url": "https://example.com/talk.mp4",
  "template_id": "newsroom",
  "aspect_ratio": "9:16"
}
From an upload
{
  "asset_id": "a059c6f4-604d-4f68-81f6-f839b9c95b0a/7afc924f-a906-4b3f-928c-5a36bed8a6fe.mp4",
  "template_id": "newsroom",
  "aspect_ratio": "9:16"
}
A caption style, with clip art and a chosen accent colour
{
  "video_url": "https://example.com/walkthrough.mp4",
  "template_id": "caption-kinetic",
  "aspect_ratio": "9:16",
  "options": {
    "clip_art": true,
    "colors": {
      "accent": "#FFD24A"
    }
  }
}
Responses
StatusMeaning
202Accepted. Poll for the result.
400Something in the request cannot be used — see code.
401The key is missing, malformed or revoked.
402Not enough credits. missing says how many short.
403This key is read-only, or it is a Brand key and its Brand does not allow this look (allowed_templates).
413The clip is longer than this product renders.
415The uploaded file is not a video we can read.
422The asset is not in storage, or is not readable as a recording.
429Too many renders in flight on this account, or too many requests on this key. retry_after says how long to wait.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
503Rendering is paused. Retry later; nothing was charged.
202 response
{
  "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
  "status": "queued"
}
GET/v1/renders/{render_id}

Where a film has got to, and the film itself

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
ParameterInTypeDescription
render_id required path string From POST /v1/renders.
Responses
StatusMeaning
200The render's state. A render belonging to another account is not found, never forbidden.
401The key is missing, malformed or revoked.
404No render with that id on this account.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
200 — Still working
{
  "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
  "status": "processing",
  "progress": 62
}
200 — Finished
{
  "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
  "status": "completed",
  "progress": 100,
  "output": {
    "video_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?X-Amz-Signature=example",
    "download_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?response-content-disposition=attachment&X-Amz-Signature=example",
    "expires_in": 7200,
    "duration_seconds": 20,
    "aspect_ratio": "9:16"
  },
  "credits_used": 20,
  "template_id": "newsroom"
}
200 — Did not finish
{
  "render_id": "3f1c1f6e-6a1b-4a1e-9f0a-2b7c9d5e8a10",
  "status": "failed",
  "progress": 0,
  "error": {
    "code": "RENDER_FAILED",
    "message": "We couldn't finish step 3 of 5.",
    "reference": "Green 7F3A"
  }
}

Account

Who the key belongs to and what it can spend.

GET/v1/me

Who this key belongs to and what it can spend

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
StatusMeaning
200The account behind this key.
401The key is missing, malformed or revoked.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
200 response
{
  "user_id": "a059c6f4-604d-4f68-81f6-f839b9c95b0a",
  "plan": "weekly",
  "credits_remaining": 281,
  "permissions": {
    "render": true,
    "read": true
  }
}

Films

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
ParameterInTypeDescription
film_id required path string From a film.ready webhook, or the render_id of a render a Brand key started.
Responses
StatusMeaning
200The film.
401The key is missing, malformed or revoked, or it is a Brand key that no longer works.
404No film with that id in this key's Brand.
500Something went wrong on our side. The body is the standard error envelope; X-Request-Id is what support needs.
200 — Ready
{
  "film_id": "8c2d7a4e-1f0b-4c6e-9a3d-5b7e2f1c0a94",
  "render_id": "8c2d7a4e-1f0b-4c6e-9a3d-5b7e2f1c0a94",
  "title": "Harvest report: the Huila microlot",
  "template_id": "newsroom",
  "aspect_ratio": "9:16",
  "duration_seconds": 94.2,
  "finished_at": "2026-09-25T14:03:11.000Z",
  "brand": {
    "id": "4a1f9e2c-7b3d-4e8a-a6c5-0d9f8e7b6a21",
    "name": "Acme Coffee"
  },
  "status": "completed",
  "output": {
    "video_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?X-Amz-Signature=example",
    "download_url": "https://remotionlambda-uswest2.s3.us-west-2.amazonaws.com/renders/n0fecl6yho/out.mp4?response-content-disposition=attachment&X-Amz-Signature=example",
    "expires_in": 7200
  }
}
200 — Being made
{
  "film_id": "8c2d7a4e-1f0b-4c6e-9a3d-5b7e2f1c0a94",
  "render_id": "8c2d7a4e-1f0b-4c6e-9a3d-5b7e2f1c0a94",
  "title": "Harvest report: the Huila microlot",
  "template_id": "newsroom",
  "aspect_ratio": "9:16",
  "duration_seconds": 94.2,
  "finished_at": null,
  "brand": {
    "id": "4a1f9e2c-7b3d-4e8a-a6c5-0d9f8e7b6a21",
    "name": "Acme Coffee"
  },
  "status": "processing"
}

Reference

Objects

The shapes above, consolidated — useful when you already know the endpoint and just need a field.

Template

FieldTypeValuesDescription
id required string newsroom · scrapbook · editorial · broadsheet · blueprint · dossier · briefing · prism · ultraEditorialPop · ultraFinancialJournal · ultraBusinessDocumentary · ultraInvestigativeDossier · ultraEvidence · ultraMindmap · ultraStoryboard · ultraMap · ultraData · ultraMindnet · ultraCountdown · ultraPlaybook · caption-kinetic · caption-scale · caption-cover · caption-floorplan · caption-marker · caption-stickers · caption-arc · caption-frame Pass this as template_id.
name required string — What a person calls this look.
tier required string pro · ultra · captions 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.
description required string — What kind of film this look makes.
best_for required array of string — Subjects this look was built for.
avoid_for required array of string — Where it will disappoint. Read this half — it is what stops a bad choice.
aspect_ratios required 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

FieldTypeValuesDescription
user_id required string (uuid) — —
plan required string — The account's current plan, or free.
credits_remaining required integer — Spendable credits. One credit is one second of finished film; every render costs at least 15.
permissions required 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.id required string (uuid) — —
brand.name required string — —
brand.looks required 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

FieldTypeValuesDescription
asset_id required string — Pass this to POST /v1/renders once the PUT has succeeded.
upload_url required string (uri) — One-time URL. PUT the bytes here.
method required string PUT —
headers object — Send these with the PUT.
expires_in required 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.

FieldTypeValuesDescription
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.
template_id required string newsroom · scrapbook · editorial · broadsheet · blueprint · dossier · briefing · prism · ultraEditorialPop · ultraFinancialJournal · ultraBusinessDocumentary · ultraInvestigativeDossier · ultraEvidence · ultraMindmap · ultraStoryboard · ultraMap · ultraData · ultraMindnet · ultraCountdown · ultraPlaybook · caption-kinetic · caption-scale · caption-cover · caption-floorplan · caption-marker · caption-stickers · caption-arc · caption-frame 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

FieldTypeValuesDescription
render_id required string (uuid) — —
status required string queued · processing · completed · failed Only completed and failed are final.
progress required 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_url required string or null (uri) — Signed, for playback. Expires.
output.download_url required string or null (uri) — Signed, with a filename attached, for saving. Expires.
output.expires_in required 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.code required string RENDER_FAILED —
error.message required string — What to show a person. Deliberately carries no internal detail.
error.reference required string — A word and four characters, e.g. Green 7F3A. Support finds the film by this.

Error

FieldTypeValuesDescription
error required object — —
error.code required string INVALID_API_KEY · FORBIDDEN · NOT_FOUND · INVALID_REQUEST · INVALID_TEMPLATE · INVALID_ASPECT_RATIO · INVALID_ASSET · UNSUPPORTED_VIDEO · FILE_TOO_LARGE · CLIP_TOO_LONG · INSUFFICIENT_CREDITS · RATE_LIMITED · TOO_MANY_RENDERS · RENDER_FAILED · RENDERING_PAUSED · INTERNAL A stable machine code. Branch on this, never on message — the wording can improve, the code cannot change.
error.message required 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
LimitValue
Render callsPOST /v1/renders — 20 per minute, per key
Read callsEverything else — 120 per minute, per key
Source file size2 GB
Source lengthUp 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.

CodeHTTP statusWhat to do
INVALID_API_KEY401Check the key was copied in full and has not been revoked in Settings → API keys. Mint a new one if in doubt.
FORBIDDEN403This 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_FOUND404Nothing 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_REQUEST400Something in the request body could not be used. Check field names, types and required fields against this page.
INVALID_TEMPLATE400That template_id does not exist. Call GET /v1/templates for the current list rather than hard-coding one.
INVALID_ASPECT_RATIO400That 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_ASSET422The 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_VIDEO415The 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_LARGE413limit_bytes and bytes on the error say the cap and the actual size. Compress or trim the source and try again.
CLIP_TOO_LONG413The clip is longer than this product renders. Trim it and try again.
INSUFFICIENT_CREDITS402required, available and missing say exactly how many credits are short. Nothing was charged; buy at least missing more.
RATE_LIMITED429Too many requests on this key. Wait retry_after seconds — also sent as the Retry-After header — and retry.
TOO_MANY_RENDERS429Too many renders already in flight on this account. Wait retry_after seconds and retry; nothing was charged.
RENDER_FAILED500The 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_PAUSED503Rendering is paused for everyone for a moment. Nothing was charged; wait and retry later.
INTERNAL500Something 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.

FieldTypeValuesDescription
error required object — —
error.code required string INVALID_API_KEY · FORBIDDEN · NOT_FOUND · INVALID_REQUEST · INVALID_TEMPLATE · INVALID_ASPECT_RATIO · INVALID_ASSET · UNSUPPORTED_VIDEO · FILE_TOO_LARGE · CLIP_TOO_LONG · INSUFFICIENT_CREDITS · RATE_LIMITED · TOO_MANY_RENDERS · RENDER_FAILED · RENDERING_PAUSED · INTERNAL A stable machine code. Branch on this, never on message — the wording can improve, the code cannot change.
error.message required 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.

ToolWhat it doesParameters
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.
look required — 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_id required — 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.