← Code Recipes
Image & Video

Runway Free code examples in Python and JavaScript

Text-to-video and image-to-video generation.

C# Python Java JavaScript

WHAT THIS PAGE COVERS

Polling a long-running video job without leaking a connection.

Model gen4.5
Package runwayml
Checked

Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.

Install

RUNWAYML_API_SECRET, not _API_KEY. The client reads it with no arguments, so RunwayML() is the whole construction.

pip install runwayml

export RUNWAYML_API_SECRET="key_your-secret"   # macOS / Linux
setx RUNWAYML_API_SECRET "key_your-secret"     # Windows, then open a new shell
Image to video

prompt_image takes a URL the API fetches itself. The ratio is pixel dimensions, not an aspect string — '1280:720', with a colon, and a mismatch against the source image is a validation error rather than a crop.

import os
from runwayml import RunwayML

client = RunwayML(api_key=os.environ["RUNWAYML_API_SECRET"])

task = client.image_to_video.create(
    model="gen4.5",
    prompt_image="https://example.com/assets/microphone.jpg",
    prompt_text="The camera slowly pushes in on the microphone",
    ratio="1280:720",
    duration=5,
)

print("submitted:", task.id)
Waiting without writing a polling loop

wait_for_task_output() does the polling at the rate Runway asks for. Writing your own loop at one second gets you rate-limited; writing one at thirty wastes half a minute per job.

from runwayml import TaskFailedError

try:
    # Chains onto create(): submits, then blocks until SUCCEEDED.
    result = client.image_to_video.create(
        model="gen4.5",
        prompt_image="https://example.com/assets/microphone.jpg",
        prompt_text="The camera slowly pushes in on the microphone",
        ratio="1280:720",
        duration=5,
    ).wait_for_task_output()

    video_url = result.output[0]
    print(video_url)

except TaskFailedError as error:
    # task_details carries the reason — usually moderation or a
    # source image the model could not use.
    print("generation failed:", error.task_details)
Not holding a web request open for it

A five-second clip takes minutes to render. Submit, store the task id, return immediately, and let a worker or a poll from the client pick it up — this is the shape that survives a load balancer.

from fastapi import FastAPI

app = FastAPI()

@app.post("/videos")
def submit(image_url: str, prompt: str):
    task = client.image_to_video.create(
        model="gen4.5",
        prompt_image=image_url,
        prompt_text=prompt,
        ratio="1280:720",
        duration=5,
    )

    # Return the id now. Nothing waits.
    return {"task_id": task.id, "status": "PENDING"}

@app.get("/videos/{task_id}")
def check(task_id: str):
    task = client.tasks.retrieve(task_id)

    return {
        "status": task.status,   # PENDING | RUNNING | SUCCEEDED | FAILED
        "url": task.output[0] if task.status == "SUCCEEDED" else None,
    }
Downloading before the link expires

The output URL is signed and temporary, like every other media API here. Fetch the bytes as soon as the task succeeds rather than storing the link.

import requests

result = client.image_to_video.create(
    model="gen4.5",
    prompt_image="https://example.com/assets/microphone.jpg",
    prompt_text="The camera slowly pushes in on the microphone",
    ratio="1280:720",
    duration=5,
).wait_for_task_output()

video = requests.get(result.output[0], stream=True, timeout=120)
video.raise_for_status()

with open("clip.mp4", "wb") as handle:
    for chunk in video.iter_content(chunk_size=1 << 16):
        handle.write(chunk)
Install

The package is scoped. The secret lives in RUNWAYML_API_SECRET and the client picks it up on its own.

npm install @runwayml/sdk

export RUNWAYML_API_SECRET="key_your-secret"
Image to video

promptImage is a URL the API fetches. ratio is pixel dimensions with a colon — '1280:720' — and not an aspect ratio string, which is the mistake the error message does not quite explain.

import RunwayML from "@runwayml/sdk";

const client = new RunwayML();

const task = await client.imageToVideo.create({
  model: "gen4.5",
  promptImage: "https://example.com/assets/microphone.jpg",
  promptText: "The camera slowly pushes in on the microphone",
  ratio: "1280:720",
  duration: 5,
});

console.log("submitted:", task.id);
Waiting without writing a polling loop

waitForTaskOutput polls at the documented rate. Runway asks for no more than one check every five seconds per task, and a hand-written loop usually gets that wrong in one direction or the other.

import { TaskFailedError } from "@runwayml/sdk";

try {
  const result = await client.imageToVideo
    .create({
      model: "gen4.5",
      promptImage: "https://example.com/assets/microphone.jpg",
      promptText: "The camera slowly pushes in on the microphone",
      ratio: "1280:720",
      duration: 5,
    })
    .waitForTaskOutput();

  console.log(result.output[0]);
} catch (error) {
  if (error instanceof TaskFailedError) {
    // Usually moderation, or a source image the model rejected.
    console.error("generation failed:", error.taskDetails);
  } else {
    throw error;
  }
}
Not holding a web request open for it

Rendering takes minutes and a request that waits will be killed by something in between. Submit, return the id, and let the client poll your own endpoint.

import express from "express";

const app = express();
app.use(express.json());

app.post("/videos", async (request, response) => {
  const task = await client.imageToVideo.create({
    model: "gen4.5",
    promptImage: request.body.imageUrl,
    promptText: request.body.prompt,
    ratio: "1280:720",
    duration: 5,
  });

  response.status(202).json({ taskId: task.id, status: "PENDING" });
});

app.get("/videos/:id", async (request, response) => {
  const task = await client.tasks.retrieve(request.params.id);

  response.json({
    status: task.status,     // PENDING | RUNNING | SUCCEEDED | FAILED
    url: task.status === "SUCCEEDED" ? task.output[0] : null,
  });
});

app.listen(3000);
Downloading before the link expires

The output URL is signed and short-lived. Stream it to disk or to object storage the moment the task succeeds — a stored URL is a broken video tomorrow.

import { createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const result = await client.imageToVideo
  .create({
    model: "gen4.5",
    promptImage: "https://example.com/assets/microphone.jpg",
    promptText: "The camera slowly pushes in on the microphone",
    ratio: "1280:720",
    duration: 5,
  })
  .waitForTaskOutput();

const video = await fetch(result.output[0]);
if (!video.ok) throw new Error(`Download failed: ${video.status}`);

// Stream it: a ten-second clip is tens of megabytes.
await pipeline(Readable.fromWeb(video.body), createWriteStream("clip.mp4"));

QUESTIONS

Which languages are the Runway examples in?

Python and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.

Which package do I install for Runway?

The samples use runwayml. The install step on each language tab gives the exact command.

Which Runway model do these examples use?

gen4.5, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.

Where is the official Runway documentation?

https://docs.dev.runwayml.com/ — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.

ALSO IN IMAGE & VIDEO

The authority

Official Runway documentation

Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.

Read the docs ↗

https://docs.dev.runwayml.com/

New drops, every week

Free tools & Claude templates, landing regularly.

We keep shipping new free tools, code recipes and Claude Code templates. Follow @freeyantra on Instagram or join the newsletter so you never miss one.

Follow @freeyantra

or get them by email

Double opt-in. Unsubscribe any time. Privacy