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

7 min read β€’ June 22, 2026

Table of contents

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

Introduction

Kling Omni is the one Kling endpoint that lets you blend several image references into a single shot and storyboard a multi-shot sequence β€” each scene with its own prompt and duration β€” in one API job, all from code against your own Kling account. Kling AI is the generative video service from Chinese short-video giant Kuaishou Technology, and useapi.net fronts it with a third-party Kling API that runs your own Kling account over a standard REST endpoint β€” no enterprise contract, no per-call billing 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.

Two model versions exist, picked with omni_version: v3 (the default) and o1. Multi-shot and VIDEO-type elements are v3 only β€” 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 VIP Kling plan.

Pricing

You keep your normal Kling website subscription and add a single flat $15/month to useapi.net that covers API access to every supported service, with no per-generation surcharge from us β€” see the core Kling tutorial’s pricing section and the Kling API overview live cost calculator.

This is the consumer-account route. Kuaishou’s official Kling API bills per generation at developer rates on a separate developer account, while useapi.net automates the consumer account you already pay for at the website subscription price.

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 required in the body only when you have more than one Kling account configured. 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); 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 both o1 and v3 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 only in v3 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();
}

const elapsedTimeSec = (start) => (Date.now() - start) / 1000;

// 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

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? Omni is a single Kling video endpoint, POST /videos/omni, that picks a workflow from your inputs: blend up to 7 image references (@image_1…@image_7), reuse saved Video Elements (@element_1…), storyboard a v3 multi-shot sequence, run a start/end-frame transition, or guide generation from a reference video. See What Omni does above.

How do I pass multiple image references to Kling? Upload each image with POST /assets, pass the returned URLs as image_1, image_2, … (up to 7), and reference them in the prompt with @image_1, @image_2, etc. 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? A Video Element is a reusable character/object reference created once with POST /elements β€” IMAGE elements from a coverImage, or VIDEO elements from an mp4 clip (v3 only). It returns an id like u_123…, which you then drop into any Omni prompt as @element_1. See Video Elements above.

How does Kling multi-shot work? On v3 Omni, set shot_1_prompt/shot_1_duration through shot_6_prompt/shot_6_duration (minimum 2 shots) instead of a single prompt/duration. Shots must be sequential with no gaps and the total duration must be 3–15 seconds. See Multi-shot sequences (v3) above.

Which Omni version supports multi-shot and Video Elements? Both are v3 only (the default). The o1 version supports IMAGE elements and single clips of 3–10s, while v3 adds VIDEO elements, multi-shot, 4k mode, and 3–15s durations. See What Omni does above.

Why is my Omni video watermarked? The MP4 at works[0].resource.resource returned by the poll is the watermarked preview. To get the clean, non-watermarked master, take the workId from the task’s works array and call GET /assets/download β€” it returns a cdnUrl to the watermark-free file. This requires a paid Kling account. See Generate a multi-reference video in two API calls above.

My generation returns a 500 β€” what does that mean? Kling reuses the 500 response for content-moderation rejections as well as genuine server faults, and the generic message field rarely makes the difference clear. Read the error text instead. If a job clears creation but the poll later returns 404, the task was deleted, failed at moderation, or your account ran out of credits.

How is this different from the official Kling API? Kuaishou’s official Kling API bills per generation at developer rates on a separate developer account. useapi.net instead automates your own consumer Kling account, so you generate at the website subscription price plus a flat $15/month β€” and the same one subscription covers 10+ other AI services.

Conclusion

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

We regularly post guides and tutorials on the YouTube Channel.

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

Cross posted