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

6 min read β€’ June 22, 2026

Table of contents

  1. Introduction
  2. Supported models
  3. Pricing
  4. Generate a video in two API calls
  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 against your own 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.

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

You keep your normal Kling website subscription for the underlying account β€” Kling starts with a free tier and a $10/month Standard plan β€” 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. Generations draw from your Kling account’s own credit balance at Kling’s standard rates, which vary by model, mode, duration, and audio. The Kling API overview has a live cost calculator that maps each model and mode to its credit cost for your plan.

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 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_txt2video_hq",
    "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.

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": 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();
}

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

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

Frequently asked questions

Is there a Kling API? Yes β€” a few ways. Kuaishou offers an official Kling developer API, and third-party resellers (fal.ai, Replicate, PiAPI, and others) expose Kling too, all billed per generation at developer rates. useapi.net is the consumer-account route: programmatic access to every Kling model by driving your own Kling account at the website subscription price through a standard REST endpoint.

How do I access Kling v3 (and v3 Turbo) through the API? 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 supports 3–15s durations, std/pro/4k modes, and audio on by default. See Supported models above.

Can I turn an image into a video (image-to-video)? 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. See Image-to-video above.

How much does the Kling API cost? You keep your normal Kling website subscription, plus a flat $15/month to useapi.net for API access to all services. Generations draw from your Kling account’s own credits at Kling’s standard rates, which vary by model, mode, duration, and audio β€” the Kling API overview has a live cost calculator. See Pricing above.

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

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.

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 β€” a moderation rejection reads like β€œThe content you uploaded appears to violate the community guidelines.” 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.

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