Overview

Making API calls

You need an API key first — see account setup.

Install an SDK#

The SDKs give you type safety and correct polling behaviour for free. Use them unless you have a reason not to.

npm install --save @runwayml/sdk
pip install runwayml

The Node.js SDK ships TypeScript bindings and supports Node 18+. The Python SDK ships MyPy-compatible type annotations and supports Python 3.8+.

Image-to-video#

Give Gen-4.5 a still image and a prompt describing the motion you want:

import RunwayML, { TaskFailedError } from '@runwayml/sdk';
 
const client = new RunwayML();
 
try {
  const task = await client.imageToVideo
    .create({
      model: 'gen4.5',
      promptImage: 'https://example.com/your-image.jpg',
      promptText: 'A timelapse on a sunny day with clouds flying by',
      ratio: '1280:720',
      duration: 5,
    })
    .waitForTaskOutput();
 
  console.log('Video URL:', task.output[0]);
} catch (error) {
  if (error instanceof TaskFailedError) {
    console.error('The video failed to generate.', error.taskDetails);
  } else {
    throw error;
  }
}

promptImage accepts an HTTPS URL, a base64 data URI, or a runway:// URI from an ephemeral upload. All three work anywhere a URL is accepted.

Text-to-video#

Gen-4.5 also generates video from text alone. Omit promptImage:

const task = await client.imageToVideo
  .create({
    model: 'gen4.5',
    promptText: 'A serene mountain landscape at sunrise with mist rolling through the valleys',
    ratio: '1280:720',
    duration: 5,
  })
  .waitForTaskOutput();

Text-to-video on Gen-4.5 supports landscape 1280:720 and portrait 720:1280 only. Image-to-video supports a wider set — see aspect ratios.

Passing a local file as a data URI#

If the image lives on disk, base64-encode it instead of uploading it somewhere first:

import fs from 'node:fs';
import RunwayML from '@runwayml/sdk';
 
const client = new RunwayML();
 
const imageBuffer = fs.readFileSync('example.png');
const dataUri = `data:image/png;base64,${imageBuffer.toString('base64')}`;
 
const task = await client.imageToVideo
  .create({
    model: 'gen4.5',
    promptImage: dataUri,
    promptText: 'A timelapse on a sunny day with clouds flying by',
    ratio: '1280:720',
    duration: 5,
  })
  .waitForTaskOutput();

Data URIs are capped at 5MB for images and 16MB for video and audio — and base64 inflates a file by about 33%, so a 5MB budget means a 3.3MB original. Above that, use ephemeral uploads, which take files up to 200MB.

Generating an image#

Stills use textToImage:

const imageTask = await client.textToImage
  .create({
    model: 'gen4_image',
    promptText: 'A beautiful sunset over a calm ocean',
    ratio: '1360:768',
  })
  .waitForTaskOutput();
 
console.log(imageTask.output[0]);

Waiting for output#

Generations are asynchronous. create returns a task ID straight away:

{ "id": "17f20503-6c24-4c16-946b-35dbbce2af2f" }

The task then moves to SUCCEEDED, FAILED or CANCELED. You can poll GET /v1/tasks/{id} yourself:

const task = await client.tasks.retrieve('17f20503-6c24-4c16-946b-35dbbce2af2f');
console.log(task.status); // "PENDING"

If you do poll manually, use an interval of 5 seconds or more, add jitter, and back off exponentially on non-200 responses. Avoid fixed-interval polling with setInterval — API latency makes the calls stack up.

Use waitForTaskOutput instead#

Every create method returns a promise carrying a waitForTaskOutput helper that polls correctly for you.

The one rule: call it on the unawaited result of create.

// ✅ Correct — chain onto create()
const imageTask = await client.textToImage
  .create({ model: 'gen4_image', promptText: 'A sunset', ratio: '1360:768' })
  .waitForTaskOutput();
// ❌ Wrong — awaiting create() first loses the helper
const awaited = await client.textToImage.create({ /* ... */ });
await awaited.waitForTaskOutput(); // not available

To keep the task ID for your own records and still wait for the result, await the create promise separately:

// Note: no await here
const imageTask = client.textToImage.create({
  model: 'gen4_image',
  promptText: 'A beautiful sunset over a calm ocean',
  ratio: '1360:768',
});
 
const taskId = (await imageTask).id; // store this in your database
const completed = await imageTask.waitForTaskOutput();
 
console.log(completed.output[0]);

tasks.retrieve carries the same helper, so you can create a task in one request and wait for it in another:

const task = await client.tasks
  .retrieve('17f20503-6c24-4c16-946b-35dbbce2af2f')
  .waitForTaskOutput();

Timeouts and cancellation#

waitForTaskOutput times out after ten minutes by default and throws TaskTimedOutError. Pass your own timeout and an AbortSignal:

const task = await client.textToImage
  .create({ model: 'gen4_image', promptText: 'A sunset', ratio: '1360:768' })
  .waitForTaskOutput({
    timeout: 5 * 60 * 1000,
    abortSignal: myAbortSignal,
  });

Passing null waits indefinitely. Don't — if you hit your concurrency limit or Runway has an outage, indefinite waits pile up on your server.

When you generate inside a request handler, abort polling if the client disconnects:

app.post('/generate-image', async (req, res) => {
  const abortController = new AbortController();
  req.on('close', () => abortController.abort());
 
  try {
    const imageTask = await runway.textToImage
      .create({ model: 'gen4_image', promptText: req.body.prompt, ratio: '1360:768' })
      .waitForTaskOutput({ abortSignal: abortController.signal });
 
    res.send(imageTask.output[0]);
  } catch (error) {
    if (error instanceof TaskFailedError) {
      res.status(500).send('Task failed');
    } else {
      throw error;
    }
  }
});

Add rate limiting to any endpoint that triggers a generation on user input — it spends your credits.

Aborting or timing out does not cancel the task. Polling stops; the generation keeps running and still costs credits. To actually stop it, call the cancellation endpoint.

Handling failure#

A task that reaches FAILED throws TaskFailedError, with the full tasks.retrieve payload on error.taskDetails:

import { TaskFailedError } from '@runwayml/sdk';
 
try {
  const imageTask = await client.textToImage
    .create({ model: 'gen4_image', promptText: 'A sunset', ratio: '1360:768' })
    .waitForTaskOutput();
} catch (error) {
  if (error instanceof TaskFailedError) {
    console.error('Task failed:', error.taskDetails);
  } else {
    throw error;
  }
}

The failureCode inside taskDetails tells you whether a retry is worth attempting — see task failures.

Endpoint-to-method map#

Operation Endpoint Node.js method
Generate an image POST /v1/text_to_image client.textToImage.create
Generate a video POST /v1/image_to_video client.imageToVideo.create
Character performance POST /v1/character_performance client.characterPerformance.create
Retrieve a task GET /v1/tasks/{id} client.tasks.retrieve
Upload a file POST /v1/uploads client.uploads.createEphemeral

The full list lives in the API reference.

  • API reference — every endpoint, with a live playground
  • Models — every model ID and what it accepts
  • Inputs — codecs, size limits and aspect ratio rules
  • Outputs — why you must re-host result URLs
  • Go-live checklist — before real users hit this code
Run it against your own key

The code on this page works as-is once RUNWAYML_API_SECRET is set.

Updated

Was this page helpful?