How to Generate AI Video with Kling v3 via the Kling API

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

Table of contents

  1. Introduction
  2. Supported models
  3. Pricing
  4. Generate a video in two API calls
    1. 1. Submit the job
    2. 2. Poll for the result
  5. Image-to-video
  6. Batch-generate with a script
  7. Examples
  8. Frequently asked questions
  9. Conclusion

Introduction

Kling AI is rated by independent text-to-video leaderboards among the strongest models for prompt adherence and motion realism, and you can drive every Kling model 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.

Supported models

Pick a model per request with the model_name field on POST /videos/text2video. kling-v1-6 is the default. Capabilities differ by version:

Model Durations Modes / resolution Audio
kling-v3-0 3–15s std 720p · pro 1080p · 4k 4K Default on
kling-v3-0-turbo 3–15s std 720p · pro 1080p Always on
kling-v2-6 5 / 10s std 720p · pro 1080p Native Audio (pro)
kling-v2-5 5 / 10s pro 1080p only Sound effects
kling-v2-1-master 5 / 10s pro 1080p only Sound effects
kling-v1-6 (default) 5 / 10s std 720p · pro 1080p Sound effects
kling-v1-5 5 / 10s std 720p · pro 1080p Sound effects

Model v3-0 is the most capable: it adds 4k mode and audio on by default. Model v3-0-turbo is the faster v3 — it keeps v3.0 quality with shorter generation times, audio always on, and std/pro modes (no 4k). Model 2.6 produces Native Audio, but only in pro mode. The negative_prompt and cfg_scale parameters apply only to the 1.x models — 2.5, 2.6, and 3.0 do not support negative_prompt, and cfg_scale is a 1.x-only control. The same account and token also drive Kling’s multi-shot storytelling, Video Elements, Avatars, and lip-sync — see the Kling API overview for the full lineup.

Pricing

Videos use the credits of your Kling AI website subscription at Kling’s own rates, which vary by model, mode, duration and audio, 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 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.

1. Submit the job

POST https://api.useapi.net/v1/kling/videos/text2video:

curl -X POST "https://api.useapi.net/v1/kling/videos/text2video" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A majestic mountain landscape with snow-capped peaks and flowing rivers, camera slowly panning right",
    "model_name": "kling-v3-0",
    "aspect_ratio": "16:9",
    "mode": "pro",
    "duration": "5"
  }'

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

{
  "task": {
    "id": 123456789,
    "type": "m2v_aio2video",
    "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. 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.

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": 1268,
        "width": 724,
        "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"

A video typically finishes in a few minutes depending on model, mode, and duration — std is faster than pro or 4k. Prefer not to poll? Pass a replyUrl in the create body to receive a webhook callback when the task completes. 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. The terminal failure statuses are 6, 7, 9, 50, 53, 54, and 58 (all status_final: true). See GET /tasks/task_id for what each one means.

Image-to-video

To animate a still image, use POST /videos/image2video-frames. Upload your image first with POST /assets, then pass the returned URL as image (the start frame) and optionally image_tail (the end frame, for a start→end transition):

curl -X POST "https://api.useapi.net/v1/kling/videos/image2video-frames" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "https://example.com/start-frame.jpg",
    "image_tail": "https://example.com/end-frame.jpg",
    "prompt": "the character turns and smiles, camera pushes in",
    "model_name": "kling-v2-5",
    "mode": "pro"
  }'

At least one of image or image_tail is required. On kling-v3-0 / kling-v3-0-turbo the aspect ratio is derived from the input image, so there is no aspect_ratio parameter — and kling-v3-0-turbo supports the start frame only (no image_tail). The response is the same task object as text-to-video, and you poll and download it exactly the same way (with GET /assets/download for the clean master).

Batch-generate with a 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 text-to-video job, 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.mjs in the same folder and run node ./kling.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.

Expand prompts.json
[
    {
        "prompt": "By default the kling-v1-6 model with a 16:9 aspect ratio, std (720p) mode and a 5-second duration will be used."
    },
    {
        "model_name": "kling-v3-0",
        "aspect_ratio": "16:9",
        "mode": "pro",
        "duration": "5",
        "prompt": "A majestic mountain landscape with snow-capped peaks and flowing rivers, camera slowly panning right. For all parameters see https://useapi.net/docs/api-kling-v1/post-kling-videos-text2video"
    },
    {
        "model_name": "kling-v3-0-turbo",
        "mode": "pro",
        "duration": "10",
        "prompt": "Faster Kling v3 Turbo generation. Audio is always on and the prompt can carry spoken dialogue and scene changes."
    },
    {
        "model_name": "kling-v1-6",
        "aspect_ratio": "9:16",
        "mode": "pro",
        "duration": "5",
        "negative_prompt": "people, buildings, text, low quality",
        "prompt": "negative_prompt and cfg_scale apply only to the 1.x models. Models 2.5, 2.6 and 3.0 do not support negative_prompt."
    }
]
Expand kling.mjs script
/*

Script version 1.0, June 22, 2026

Script to batch-generate videos using prompts with the Kling API v1 by useapi.net 🚀
Defaults to the kling-v1-6 model and uses the asynchronous text2video endpoint.
For more details visit https://useapi.net/docs/api-kling-v1/post-kling-videos-text2video

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

Example:
--------

node kling.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_results.txt';
const ERRORS_FILE = 'kling_errors.txt';
const DEFAULT_PROMPTS_FILE = 'prompts.json';
const DEFAULT_MODEL = 'kling-v1-6';
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 urlText2Video = 'https://api.useapi.net/v1/kling/videos/text2video';
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();
}

// Submit a single text-to-video prompt. Returns { status, taskId }.
async function submitVideo(apiToken, email, prompt, index) {
    const { model_name, prompt: text, aspect_ratio, mode, duration, negative_prompt, cfg_scale } = prompt;

    const useModel = model_name ?? DEFAULT_MODEL;

    console.log(`🚀 ${useModel} » Prompt #${index} • ${email} • ${duration ?? 5}s …`);

    const body = JSON.stringify({
        email,
        model_name: useModel,
        prompt: text,
        aspect_ratio,
        mode,
        duration,
        negative_prompt,
        cfg_scale
    });

    const createResponse = await fetch(urlText2Video, {
        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}:${text ?? ''}\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}:${text ?? ''}\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}:${text ?? ''}\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}:${text ?? ''}\n`);
                break;
            default:
                console.log(`❗ FAILED with HTTP ${createResponse.status}`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${text ?? ''}\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_${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.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 text2video endpoint.
    // See https://useapi.net/docs/api-kling-v1/post-kling-videos-text2video for the full parameter set.
    const supportedParams = ['model_name', 'prompt', 'aspect_ratio', 'mode', 'duration', 'negative_prompt', 'cfg_scale'];

    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 } = prompt;

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

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

        if (text && text.length > 2500)
            warnings.push(`⚠️  prompt exceeds 2500 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 clip below is a real Kling generation produced through this Kling API, straight from our blog walkthrough.

Kling v3 Turbo — kling-v3-0-turbo, image-to-video, 1080p 10s with spoken dialogue and scene cuts

— from Kling 3.0 Turbo: faster video with dialogue and scene changes

Related tutorials: Kling 4.0 video, multi-reference and multi-shot video with Kling Omni and Kling lip-sync avatars and motion control.

Frequently asked questions

Is there a Kling API? Through useapi.net, yes: the Kling API runs every Kling model on your Kling AI website subscription through a standard REST endpoint, Kling 4.0 included, which Kling’s own developer API doesn’t offer yet (as of October 18, 2026). Connect your Kling account once with Setup Kling and call it with a useapi.net token.

How do I access Kling v3 (and v3 Turbo) through the API? Through useapi.net, pass "model_name": "kling-v3-0", or "kling-v3-0-turbo" for the faster variant, to POST /videos/text2video or POST /videos/image2video-frames. Kling v3 runs 3–15s with std, pro and 4k modes and audio on by default, and Turbo keeps v3 quality with shorter generation times and no 4k. See Supported models above.

Can I turn an image into a video (image-to-video)? Through useapi.net, yes: upload the still with POST /assets, then pass the returned URL as image (start frame) and optionally image_tail (end frame) to POST /videos/image2video-frames. On Kling v3 the aspect ratio comes from the image, and v3 Turbo takes the start frame only. See Image-to-video above.

How much does the Kling API cost? Through useapi.net, on Kling Pro ($32.56/month for 3,000 credits), Kling 3.0 at 1080p with audio costs about $0.13 a second, against about $0.17 on Kling’s developer API, which sells video packs from $700 (as of October 2026). Kling 4.0 runs about $0.10 a second at 720p and $0.15 at 1080p on the same plan. The Kling API overview has a live cost calculator for every model.

Why is my 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 video in two API calls above.

How is this different from the official Kling API? Through useapi.net, one API token covers your Kling accounts and 10+ other AI services, with no Kling developer account to open, and the same token also runs Kling’s lip-sync, avatar video, effects and virtual try-on endpoints. Kuaishou’s official Kling API needs a separate developer account instead and bills every generation at developer rates.

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: a rejection reads like “The content you uploaded appears to violate the community guidelines.” A later 404 on the poll means the task was deleted, failed at moderation, or the account ran out of credits.

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.

The same calls are available as tools on the hosted Kling MCP server for Claude Code, Cursor and VS Code.

Cross posted

Also published on dev.to.