How to Generate Multi-Reference & Multi-Shot Video with Kling Omni via the Kling API

7 min read • June 22, 2026 (October 20, 2026)

Table of contents

  1. Introduction
  2. What Omni does
  3. Pricing
  4. Generate a multi-reference video in two API calls
    1. 1. Submit the job
    2. 2. Poll for the result
  5. Multi-shot sequences (v3)
  6. Video Elements
  7. Batch script
  8. Examples
  9. Frequently asked questions
  10. Conclusion

Introduction

Kling Omni blends several image references into a single shot and storyboards a multi-shot sequence, each scene with its own prompt and duration, in one API job, from code on your Kling AI website subscription. Kling AI is the video service of the Chinese short-video company Kuaishou Technology, and the third-party useapi.net Kling API runs it over a standard REST endpoint, with no enterprise contract and no per-video charge from us. This guide covers Omni specifically. For plain text-to-video and start/end-frame image-to-video, see the core Kling tutorial.

What Omni does

POST /videos/omni is a single endpoint that selects a workflow from the inputs you pass. Three of those are what set Omni apart from the plain text-to-video and frames endpoints:

  • Multi-image reference — pass up to 7 reference images (image_1…image_7) and weave each into the prompt with @image_1, @image_2, … syntax, so one shot can combine a character, a prop, and a background.
  • Video Elements — reusable saved character/object references created once with POST /elements, then dropped into any later generation as @element_1 (or @object_1). Images and elements share the same pool of 7 slots, so the combined total can’t exceed 7.
  • Multi-shot (v3 only) — split one video into 2–6 sequential shots, each with its own shot_N_prompt and shot_N_duration, for storytelling with scene cuts in a single job.

Omni also handles a frames workflow (frame_start/frame_end) and a video-reference/transform workflow (video_1), but those overlap with the core endpoints — this guide focuses on the multi-reference, Video Elements, and multi-shot paths.

Three model versions exist, picked with omni_version: v3 (the default), o1 and v4 (Kling 4.0, see the Kling 4.0 tutorial). Multi-shot is v3 only, and VIDEO-type elements work on v3 and v4 — o1 supports IMAGE elements and single-clip durations of 3–10s, while v3 runs 3–15s. Pick quality with mode: std (720p), pro (1080p), or 4k (v3 only). Durations of 7s and up, counts of 2–4, and 4K need a paid Kling AI website subscription.

Pricing

Omni videos use the credits of your Kling AI website subscription at Kling’s own rates, and useapi.net adds no per-video charge. Kuaishou’s official Kling API is billed separately, on a developer account with prepaid packs. The Kling API overview has a live cost calculator for every model on each Kling plan.

Generate a multi-reference video in two API calls

You need a useapi.net API token and a connected Kling account — export the token so the curl examples below run as-is:

export USEAPI_TOKEN="user:1234-..."

Generation is asynchronous — the create call returns a task object immediately, then you poll until the video is ready.

First, upload each reference image with POST /assets (raw bytes, an image Content-Type, max 10 MB, 300px minimum). It returns a Kling-hosted url:

curl "https://api.useapi.net/v1/kling/assets/[email protected]" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @character.jpg
{
  "status": 3,
  "url": "https://s21-kling.klingai.com/....jpg",
  "fileName": "abc123def456789.jpg"
}

1. Submit the job

POST https://api.useapi.net/v1/kling/videos/omni. Pass each uploaded URL as image_1, image_2, … and reference them by name in the prompt:

curl -X POST "https://api.useapi.net/v1/kling/videos/omni" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A woman @image_1 walking through the garden @image_2, holding the lantern @image_3, camera pushing in slowly",
    "omni_version": "v3",
    "mode": "pro",
    "aspect_ratio": "16:9",
    "duration": "5",
    "image_1": "https://s21-kling.klingai.com/.../character.jpg",
    "image_2": "https://s21-kling.klingai.com/.../garden.jpg",
    "image_3": "https://s21-kling.klingai.com/.../lantern.jpg"
  }'

The response returns immediately with a task object of type m2v_omni_video. The task id you poll on is task.id (a number):

{
  "task": {
    "id": 123456789,
    "type": "m2v_omni_video",
    "status": 5,
    "status_name": "submitted",
    "status_final": false
  },
  "works": [],
  "status": 5,
  "status_name": "submitted",
  "status_final": false,
  "message": ""
}

The email field is optional: without it, the API picks one of your Kling accounts that has a free job slot (429 when every account is busy). With more than one account, note which account ran the job (GET tasks/task_id explains how to find it), because polling the task needs that account’s email, and pass the email of the account that saved your elements when a prompt uses @element_N. A 500 from Kling almost always means a content-moderation rejection rather than a server fault — read the error text (the message field is generic and often misleading) to tell them apart. prompt maxes out at 1700 characters and references each input by its @-name (@image_1, @element_1, @video_1).

2. Poll for the result

GET https://api.useapi.net/v1/kling/tasks/{task_id}:

curl "https://api.useapi.net/v1/kling/tasks/[email protected]" \
  -H "Authorization: Bearer $USEAPI_TOKEN"

The task is done when status_final is true. Success is status_name: "succeed" (status: 99), and the MP4 is in works[0].resource.resource:

{
  "status": 99,
  "status_name": "succeed",
  "status_final": true,
  "works": [
    {
      "workId": 123456789,
      "status_name": "succeed",
      "resource": {
        "resource": "https://s21-kling.klingai.com/....mp4",
        "height": 720,
        "width": 1280,
        "duration": 5041
      }
    }
  ]
}

The MP4 at works[0].resource.resource is watermarked. To get the clean, non-watermarked master, take the workId from the works array and call GET /assets/download — it returns a cdnUrl to the file (a single workId plus a single fileTypes value yields a direct MP4 link, otherwise a .zip):

curl "https://api.useapi.net/v1/kling/assets/[email protected]&workIds=123456789&fileTypes=MP4" \
  -H "Authorization: Bearer $USEAPI_TOKEN"

On the poll, a 404 means the task was deleted, failed at moderation, or your Kling account ran out of credits — check your balance at GET /accounts/email. Prefer not to poll? Pass a replyUrl in the create body to receive a webhook callback when the task completes.

Multi-shot sequences (v3)

Instead of one prompt and one duration, v3 Omni lets you storyboard a single video as 2–6 sequential shots, each with its own shot_N_prompt and matching shot_N_duration. The total of all durations must land between 3 and 15 seconds, shots must be sequential with no gaps (shot_1 + shot_2, never shot_1 + shot_3), and the multi-shot parameters cannot be combined with prompt or duration. Each shot prompt can carry the same @image_N / @element_N references as a single-shot job, so a recurring character holds across cuts:

curl -X POST "https://api.useapi.net/v1/kling/videos/omni" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "omni_version": "v3",
    "mode": "pro",
    "aspect_ratio": "16:9",
    "image_1": "https://s21-kling.klingai.com/.../kitchen.jpg",
    "shot_1_prompt": "Cinematic medium shot, a chef standing behind a stainless steel stove @image_1, focused intently on the pan",
    "shot_1_duration": "3",
    "shot_2_prompt": "Continuous scene at the same stove @image_1. The chef flips the food high into the air, then turns sharply to the camera",
    "shot_2_duration": "3"
  }'

The response, polling, and clean download are identical to the single-shot flow above — the create call returns the same task.id, and the finished sequence comes back as one MP4 in works[0].

Video Elements

A Video Element is a reusable character or object reference you create once and drop into any later Omni job by ID — so the same face, costume, or prop stays consistent across separate generations without re-uploading and re-describing it each time. Create one with POST /elements. There are two types:

  • IMAGE elements — built from a coverImage URL (optionally with extra angle views, or AI-generated multi-angle views via generateViews). Usable in o1, v3 and v4 Omni.
  • VIDEO elements — built from a video URL (mp4, minimum 3 seconds, auto-trimmed to 8s), which captures motion and can carry a voice. VIDEO elements work in v3 and v4 Omni.

Upload the cover image (or clip) with POST /assets first, then register the element:

curl -X POST "https://api.useapi.net/v1/kling/elements" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "FashionLady",
    "coverImage": "https://s21-kling.klingai.com/.../character.jpg",
    "tag": "character"
  }'

It returns a generated element id (a 5-character random suffix is appended to your name):

{
  "elements": [
    {
      "id": "u_123456789012345",
      "name": "FashionLady ABC12",
      "description": "Elegant woman in red dress"
    }
  ],
  "count": 1
}

The tag is one of character, animal, prop, costume, scene, effect, or others (from GET /elements/tags) — leave it out to have Kling auto-detect it. List your saved elements anytime with GET /elements. Then reference the element by ID in any Omni prompt, alone or alongside images (remember the combined 7-slot limit):

curl -X POST "https://api.useapi.net/v1/kling/videos/omni" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Character @element_1 walking through a sunlit garden, smiling",
    "omni_version": "v3",
    "mode": "std",
    "duration": "5",
    "element_1": "u_123456789012345"
  }'

A character-tagged element can also carry a voice — an official voice name/ID from GET /elements/voices, or a 5–60s mp4 to clone one from. For a VIDEO element, voice is extracted automatically from the source clip when it has 5–60 seconds of audio.

Batch script

Finding the right shot takes many attempts, and running them by hand is tedious. The Node.js script below reads a list of prompts from prompts.json, submits each one to the Omni endpoint — single-shot (prompt + image/element refs) or multi-shot (shots) — then polls every task until it is final and downloads the finished MP4, preferring the clean, non-watermarked master via GET /assets/download and falling back to the watermarked works[0].resource.resource if needed. So you can queue a batch and come back to the winners.

You need Node.js v21 or newer. Put prompts.json and kling-omni.mjs in the same folder and run node ./kling-omni.mjs API_TOKEN EMAIL, where API_TOKEN is your useapi.net API token and EMAIL is your connected Kling account email. The script looks the account up by email automatically. Pass image URLs (already uploaded via POST /assets) as image_1…image_7, and saved element IDs as element_1…element_7.

Expand prompts.json
[
    {
        "prompt": "A single-shot Omni job: by default the v3 model with a 16:9 aspect ratio, std (720p) mode and a 5-second duration is used."
    },
    {
        "omni_version": "v3",
        "mode": "pro",
        "aspect_ratio": "16:9",
        "duration": "5",
        "image_1": "https://s21-kling.klingai.com/.../character.jpg",
        "image_2": "https://s21-kling.klingai.com/.../garden.jpg",
        "prompt": "Multi-image reference: A woman @image_1 walking through the garden @image_2, camera pushing in. For all parameters see https://useapi.net/docs/api-kling-v1/post-kling-videos-omni"
    },
    {
        "omni_version": "v3",
        "mode": "std",
        "duration": "5",
        "element_1": "u_123456789012345",
        "prompt": "Saved Video Element by ID: Character @element_1 walking through a sunlit garden, smiling."
    },
    {
        "omni_version": "v3",
        "mode": "pro",
        "aspect_ratio": "16:9",
        "image_1": "https://s21-kling.klingai.com/.../kitchen.jpg",
        "shots": [
            { "prompt": "Cinematic medium shot, a chef standing behind a stainless steel stove @image_1, focused intently on the pan.", "duration": "3" },
            { "prompt": "Continuous scene at the same stove @image_1. The chef flips the food high into the air, then turns sharply to the camera.", "duration": "3" }
        ]
    }
]
Expand kling-omni.mjs script
/*

Script version 1.0, June 22, 2026

Script to batch-generate videos using prompts with the Kling API v1 by useapi.net 🚀
Uses the asynchronous Omni endpoint (POST /videos/omni): multi-image reference,
saved Video Elements, and v3 multi-shot sequences.
For more details visit https://useapi.net/docs/api-kling-v1/post-kling-videos-omni

Installation Instructions:
==========================

You need Node.js v21 or newer installed to run this script. Download and install Node.js from:

- Windows, macOS, Linux: https://nodejs.org/

After installation, verify by running the following command in a terminal:

   node -v

Running the Script:
===================

Usage: node kling-omni.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]

Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
Replace EMAIL with configured Kling email account, see https://useapi.net/docs/start-here/setup-kling
If optional PROMPTS_FILE not provided prompts.json will be used.

Upload reference images first via POST /assets (https://useapi.net/docs/api-kling-v1/post-kling-assets)
and pass the returned URLs as image_1..image_7. Pass saved element IDs as element_1..element_7
(create them via POST /elements, https://useapi.net/docs/api-kling-v1/post-kling-elements).

Example:
--------

node kling-omni.mjs user:1234-abcdefhijklmnopqrstuv [email protected]

This command executes the script using API token user:1234-abcdefhijklmnopqrstuv with [email protected] Kling account email.

Changelog:
==========

- June 22, 2026: Initial release.

*/

import readline from 'node:readline';
import fs from 'fs/promises';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';


// Constants
const RESULTS_FILE = 'kling_omni_results.txt';
const ERRORS_FILE = 'kling_omni_errors.txt';
const DEFAULT_PROMPTS_FILE = 'prompts.json';
const SLEEP_429 = 10 * 1000; // in milliseconds
const MAX_429_RETRIES = 6;   // give up a prompt after this many consecutive 429s (all accounts busy)
const SLEEP_POLL = 20 * 1000; // in milliseconds

const urlAccounts = 'https://api.useapi.net/v1/kling/accounts';
const urlOmni = 'https://api.useapi.net/v1/kling/videos/omni';
const urlTask = 'https://api.useapi.net/v1/kling/tasks/';
const urlAssetsDownload = 'https://api.useapi.net/v1/kling/assets/download';

// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Function to fetch configured Kling API accounts
async function fetchAccounts(apiToken) {
    const response = await fetch(urlAccounts, {
        headers: {
            'Accept': 'application/json',
            'Authorization': `Bearer ${apiToken}`
        }
    });

    if (!response.ok) {
        console.error(`⛔ Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
        process.exit(1);
    }

    return response.json();
}

// A short label for logging — the single prompt, or the first shot prompt.
function promptLabel(prompt) {
    if (prompt.prompt) return prompt.prompt;
    if (Array.isArray(prompt.shots) && prompt.shots.length) return prompt.shots[0].prompt ?? '';
    return '';
}

// Build the request body for a single Omni job.
// Multi-shot: an array of { prompt, duration } in prompt.shots → shot_N_prompt / shot_N_duration.
// Single-shot: prompt + optional duration. Both carry image_N / element_N references.
function buildBody(email, prompt) {
    const { prompt: text, omni_version, mode, aspect_ratio, duration, count, shots } = prompt;

    const body = {
        email,
        omni_version,
        mode,
        aspect_ratio,
        count
    };

    // Pass through any image_1..image_7 and element_1..element_7 references.
    for (let i = 1; i <= 7; i++) {
        if (prompt[`image_${i}`]) body[`image_${i}`] = prompt[`image_${i}`];
        if (prompt[`element_${i}`]) body[`element_${i}`] = prompt[`element_${i}`];
    }

    if (Array.isArray(shots) && shots.length) {
        // Multi-shot (v3 only) — cannot be combined with prompt/duration.
        shots.forEach((shot, idx) => {
            body[`shot_${idx + 1}_prompt`] = shot.prompt;
            body[`shot_${idx + 1}_duration`] = shot.duration;
        });
    } else {
        body.prompt = text;
        body.duration = duration;
    }

    return JSON.stringify(body);
}

// Submit a single Omni prompt. Returns { status, taskId }.
async function submitVideo(apiToken, email, prompt, index) {
    const useVersion = prompt.omni_version ?? 'v3';
    const label = promptLabel(prompt);

    console.log(`🚀 omni ${useVersion} » Prompt #${index} • ${email} …`);

    const body = buildBody(email, prompt);

    const createResponse = await fetch(urlOmni, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiToken}`
        },
        body
    });

    const createBody = await createResponse.text();

    if (createResponse.status == 200) {
        const json = JSON.parse(createBody);
        const taskId = json?.task?.id;
        if (taskId) {
            await fs.appendFile(RESULTS_FILE, `${taskId},#${index}:${label}\n`);
            console.log(`✅ task.id`, taskId);
            return { status: 200, taskId };
        } else {
            const error = `No task.id found in HTTP 200 response`;
            console.log(`❓ ${error}`, createBody);
            await fs.appendFile(ERRORS_FILE, `${error},#${index}:${label}\n`);
            return { status: 500 };
        }
    } else {
        switch (createResponse.status) {
            case 429:
                console.log(`🔄️ Retry on HTTP ${createResponse.status} (all accounts at capacity)`);
                break;
            case 400:
                console.log(`🛑 Validation error`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${label}\n`);
                break;
            case 500:
                // Kling returns 500 for content moderation as well as real server faults.
                console.log(`🛑 Rejected (likely content moderation — check the error text)`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${label}\n`);
                break;
            default:
                console.log(`❗ FAILED with HTTP ${createResponse.status}`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${label}\n`);
        }
        return { status: createResponse.status };
    }
}

// Resolve the clean, non-watermarked master URL for a workId via GET /assets/download.
// Returns the cdnUrl, or undefined when unavailable.
async function fetchCleanUrl(apiToken, email, workId) {
    const url = `${urlAssetsDownload}?email=${encodeURIComponent(email)}&workIds=${workId}&fileTypes=MP4`;
    try {
        const response = await fetch(url, {
            headers: {
                'Accept': 'application/json',
                'Authorization': `Bearer ${apiToken}`
            }
        });
        if (response.ok) {
            const json = await response.json();
            if (json?.cdnUrl)
                return json.cdnUrl;
        } else {
            console.log(`⚠️  assets/download HTTP ${response.status} for workId ${workId}, falling back to watermarked`, await response.text());
        }
    } catch (err) {
        console.log(`⚠️  assets/download error for workId ${workId}, falling back to watermarked`, err);
    }
    return undefined;
}

// Poll every submitted task until it is final, then download the result.
async function download(apiToken, email) {
    if (! await fileExists(RESULTS_FILE)) return;

    try {
        const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
        const lines = resultsContent.trim().split('\n');

        for (const line of lines) {
            const [taskId, prompt] = line.split(',');
            const videoFilename = `kling_omni_${taskId}.mp4`;

            console.log(`👉 ${taskId}`);

            try {
                await fs.access(videoFilename);
                console.log(`⚠️ ${videoFilename} already exists. Skipping download.`);
                continue;
            } catch {
                // File does not exist, proceed with downloading
            }

            while (true) {
                const response = await fetch(`${urlTask}${taskId}?email=${encodeURIComponent(email)}`, {
                    headers: {
                        'Accept': 'application/json',
                        'Authorization': `Bearer ${apiToken}`
                    }
                });

                if (!response.ok) {
                    // 404 = task deleted, failed at moderation, or out of credits.
                    console.log(`🛑 Poll failed ${taskId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
                    break;
                }

                const task = await response.json();
                const { status, status_name, status_final, works, error, message } = task;

                if (status_final && status_name !== 'succeed') {
                    console.error(`🛑 FAILED ${taskId} (status ${status} ${status_name}${error ? ` — ${error}` : ''}${message ? ` — ${message}` : ''}):\n${prompt}\n`);
                    break;
                }

                if (status_final && status_name === 'succeed') {
                    const work = works?.[0];
                    const watermarkedUrl = work?.resource?.resource;
                    const workId = work?.workId;

                    // Prefer the clean master; fall back to the watermarked resource.
                    const cleanUrl = workId ? await fetchCleanUrl(apiToken, email, workId) : undefined;
                    const url = cleanUrl ?? watermarkedUrl;

                    if (url) {
                        console.log(`✅ Downloading ${cleanUrl ? 'clean master' : 'watermarked'} ${url} to ${videoFilename}`);
                        try {
                            const videoResponse = await fetch(url);
                            if (!videoResponse.ok) {
                                console.error(`⛔ Unable to download ${taskId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
                                break;
                            }
                            const stream = Readable.fromWeb(videoResponse.body);
                            await writeFile(videoFilename, stream);
                        } catch (err) {
                            console.error(`⛔ Error during download: ${err}`);
                        }
                    } else
                        console.error(`🛑 Unable to download ${taskId}, no resource URL in succeeded task:\n${prompt}\n`);

                    break;
                }

                console.log(`⌛ ${taskId} status (${status_name}) and is still in progress, waiting…`);
                await sleep(SLEEP_POLL);
            }
        }
    } catch (error) {
        console.log(`⛔ Error during download:`, error.stack || error);
    }
}

// Main function
async function main() {
    const apiToken = process.argv[2];
    const email = process.argv[3];
    const promptFile = process.argv[4] || DEFAULT_PROMPTS_FILE;

    if (!apiToken || !email) {
        console.error('Usage: node kling-omni.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]');
        process.exit(1);
    }

    console.info('Script v1.0');

    console.info('Node version is: ' + process.version);

    try {
        if (await fileExists(RESULTS_FILE)) {
            let user_input;
            while (!['y', 'n'].includes(user_input)) {
                user_input = (await promptUser(`❔ ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
                if (user_input == 'y') {
                    await download(apiToken, email);
                    await fs.unlink(RESULTS_FILE);
                }
            }
        }

        const start = new Date();
        try {
            console.info('START EXECUTION', start);
            await execute(apiToken, email, promptFile);
        }
        finally {
            console.info('COMPLETED', new Date());
            console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
        }

        try {
            console.info('START DOWNLOAD', start);
            await download(apiToken, email);
        }
        finally {
            console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
        }
    } catch (error) {
        console.error('⛔ Error during execution:', error.stack || error);
    }
}

async function execute(apiToken, email, promptFile) {
    const accounts = await fetchAccounts(apiToken);

    const accountList = Object.values(accounts);

    console.info(`Configured Kling API accounts (${accountList.length}):`, accountList.map(a => a.email).join(', '));

    if (accountList.length <= 0) {
        console.error(`⛔ No configured Kling accounts found. Please refer to https://useapi.net/docs/start-here/setup-kling`);
        process.exit(1);
    }

    // Match the account by email.
    const matched = accountList.find(a => a.email === email);

    if (!matched) {
        console.error(`⛔ Account with email ${email} not found. Please refer to https://useapi.net/docs/start-here/setup-kling`);
        process.exit(1);
    }

    const promptData = await fs.readFile(promptFile, 'utf8');
    const prompts = JSON.parse(promptData);
    console.log(`Total number of prompts to process`, prompts.length);

    let warnings = [];

    // Parameters accepted by this script for the Omni endpoint.
    // See https://useapi.net/docs/api-kling-v1/post-kling-videos-omni for the full parameter set.
    const baseParams = ['prompt', 'omni_version', 'mode', 'aspect_ratio', 'duration', 'count', 'shots'];
    const refParams = (n) => [`image_${n}`, `element_${n}`];
    const supportedParams = [...baseParams, ...[1, 2, 3, 4, 5, 6, 7].flatMap(refParams)];

    const invalidKeys = (prompt) => Object.keys(prompt).filter(key => !key.startsWith('__') && !supportedParams.includes(key))

    for (let i = 1; i <= prompts.length; i++) {
        const prompt = prompts[i - 1];
        const { prompt: text, shots } = prompt;

        const notSupported = invalidKeys(prompt);
        if (notSupported.length)
            warnings.push(`⚠️  Following params not supported: ${notSupported.join(',')}. Prompt ${i}`);

        const hasShots = Array.isArray(shots) && shots.length > 0;

        if (!text && !hasShots)
            warnings.push(`⚠️  Please specify a prompt or a shots array. Prompt ${i}`);

        if (text && hasShots)
            warnings.push(`⚠️  prompt and shots cannot be combined — multi-shot replaces the single prompt. Prompt ${i}`);

        if (hasShots) {
            if (shots.length < 2 || shots.length > 6)
                warnings.push(`⚠️  multi-shot requires 2 to 6 shots. Prompt ${i}`);
            const total = shots.reduce((sum, s) => sum + Number(s.duration || 0), 0);
            if (total < 3 || total > 15)
                warnings.push(`⚠️  total shot duration must be 3-15 seconds (got ${total}). Prompt ${i}`);
            shots.forEach((s, idx) => {
                if (!s.prompt || !s.duration)
                    warnings.push(`⚠️  each shot needs both prompt and duration (shot ${idx + 1}). Prompt ${i}`);
            });
        }

        if (text && text.length > 1700)
            warnings.push(`⚠️  prompt exceeds 1700 characters. Prompt ${i}`);
    }

    if (warnings.length > 0) {
        warnings.forEach(warning => console.warn(warning));
        console.error(`⛔ Execution stopped due to warnings.`);
        process.exit(1);
    }

    for (let i = 0; i < prompts.length; i++) {
        const prompt = prompts[i];
        let retries429 = 0;
        while (true) {
            const { status } = await submitVideo(apiToken, email, prompt, i + 1);
            if (status == 429) {
                if (++retries429 > MAX_429_RETRIES) {
                    console.error(`⛔ Gave up on prompt #${i + 1} after ${MAX_429_RETRIES} retries — all accounts still busy.`);
                    await fs.appendFile(ERRORS_FILE, `429 (gave up after ${MAX_429_RETRIES} retries),#${i + 1}\n`);
                    break;
                }
                await sleep(SLEEP_429);
            }
            else
                break;
        }
    }
}

// Utility function to check if a file exists
async function fileExists(path) {
    try {
        await fs.access(path);
        return true;
    } catch {
        return false;
    }
}

// Function to prompt user input
async function promptUser(query) {
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise((resolve) => rl.question(query, answer => {
        rl.close();
        resolve(answer);
    }));
}

function diffInMinutesAndSeconds(date1, date2) {
    const diffInSeconds = Math.floor((date2 - date1) / 1000);
    return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};

main();

Examples

The clips below are real Kling Omni generations produced through this Kling API, straight from our blog walkthroughs.

Multi-shot v3 — text-only 2-shot sequence via POST /videos/omni (omni_version: v3)

— from Kling v3: Multi-Shot Storytelling

Also on YouTube: Mission: Kitchen Impossible • Kling v3 multi-shot.

Video Elements v3 — two VIDEO elements + a background image, 720p ~11s via POST /videos/omni (omni_version: v3, mode: std)

— from Kling v3: 4K Resolution and Video Elements

Frequently asked questions

What is Kling Omni? Through useapi.net, Omni is one Kling video endpoint, POST /videos/omni, that picks a workflow from your inputs: blend several image references, reuse saved Video Elements (@element_1…), storyboard a v3 multi-shot sequence, run a start/end-frame transition, or guide the video from a reference clip. See What Omni does above.

How do I pass multiple image references to Kling? Through useapi.net, upload each image with POST /assets, pass the returned URLs as image_1, image_2 and so on (up to 7), and name them in the prompt as @image_1, @image_2. Images and saved elements share the same pool of 7 slots, so the combined total can’t exceed 7. See Generate a multi-reference video in two API calls above.

What are Video Elements and how do I reuse a character? Through useapi.net, a Video Element is a reusable character or object reference created once with POST /elements, as an IMAGE element from a coverImage or a VIDEO element from an mp4 clip. It returns an id like u_123…, which you then drop into any Omni prompt as @element_1, so the same face or costume holds across separate videos. See Video Elements above.

How does Kling multi-shot work? Through useapi.net, on v3 Omni you set shot_1_prompt/shot_1_duration through shot_6_prompt/shot_6_duration (at least 2 shots) instead of a single prompt/duration. Shots must be sequential with no gaps, the total must be 3–15 seconds, and each shot prompt can carry the same @image_N and @element_N references, so a character holds across cuts. See Multi-shot sequences (v3) above.

Which Omni version supports multi-shot and Video Elements? Through useapi.net, multi-shot runs on v3 (the default) only, and VIDEO elements on v3 and v4 (Kling 4.0). The older o1 takes IMAGE elements and shorter single clips, while v3 also adds 4k mode and longer clips. Pick the version with omni_version. See What Omni does above.

Why is my Omni video watermarked? Through useapi.net, the MP4 at works[0].resource.resource is Kling’s watermarked preview. For the clean master, take the workId from the task’s works array and call GET /assets/download, which returns a cdnUrl to the file without the watermark. This needs a paid Kling AI website subscription. See Generate a multi-reference video in two API calls above.

My generation returns a 500 — what does that mean? Through useapi.net, a 500 from Kling usually means a content-moderation rejection rather than a server fault, because Kling reuses that code for both and its message field rarely tells them apart. Read the error text instead. If a job clears creation but the poll later returns 404, the task was deleted, failed at moderation, or the account ran out of credits.

How is this different from the official Kling API? Through useapi.net, Omni runs on your Kling AI website subscription, Kling 4.0 Omni (omni_version: v4) included, which Kling’s developer API doesn’t offer yet (as of October 18, 2026). One useapi.net token also covers 10+ other AI services. Kuaishou’s official Kling API bills every generation at developer rates on a separate developer account, with video packs from $700 (as of October 2026).

Conclusion

Visit our Discord Server or Telegram Channel for any support questions and concerns.

The full runnable example is in the kling-api GitHub repo.

Cross posted

Also published on dev.to.