# Kling Image To Video

> Generate AI videos from static images using Kling's Image-to-Video API with audio-visual sync. Use this skill whenever the user wants to animate an image, convert a photo to video, create an AI video from a picture, add motion to a still image, generate video from image with camera movement or voice, or use Kling image2video — even if they don't say "Kling" explicitly.

- Skill: `qq5855144/kling-image-to-video` (Agent Skill)
- Install (CLI): `npx skillmds@latest add qq5855144/kling-image-to-video`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qq5855144/kling-image-to-video/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: qq5855144 (https://skillmd.com/u/qq5855144)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qq5855144/kling-image-to-video

---


# Kling Image-to-Video (Audio-Visual Sync)

Generate high-quality videos from static images using Kling AI. Supports multiple model versions (kling-v1 through kling-v2-6), professional quality mode, camera control, motion brush trajectories, end-frame control, and voice synthesis. Videos can be 5s or 10s.

This is an **async** skill: submitting the task returns a `task_id` immediately; the video becomes available only after polling until `task_status` is `succeed`.

> Read `references/create-task-api.md` for the full submit-task specification and Edge Function code.
> Read `references/query-task-api.md` for the full query-task specification and Edge Function code.

---

## End-to-End Workflow

### Generation-time usage (Agent direct call)

Use this pattern when working directly as an agent (no application frontend needed).

```typescript
const apiKey = process.env["INTEGRATIONS_API_KEY"]!; // platform_managed — injected by platform

// Step 1: Submit the image-to-video task
async function submitImage2VideoTask(params: {
  image: string;           // Required: Base64 string or accessible URL (jpg/jpeg/png, max 10MB)
  prompt?: string;         // Optional: text description, max 2500 chars
  model_name?: string;     // Optional: default "kling-v2-6"
  mode?: "std" | "pro";   // Optional: default "pro"
  duration?: "5" | "10";  // Optional: default "5"
  image_tail?: string;     // Optional: end-frame control image
  negative_prompt?: string;
  cfg_scale?: number;      // [0,1], not supported in v2.x
  voice_list?: { voice_id: string }[];
  sound?: "on" | "off";
  static_mask?: string;    // Optional: static motion brush mask image (Base64 or URL)
  camera_control?: {
    type: "simple" | "down_back" | "forward_up" | "right_turn_forward" | "left_turn_forward";
    config?: { horizontal?: number; vertical?: number; pan?: number; tilt?: number; roll?: number; zoom?: number };
  };
  external_task_id?: string;
  callback_url?: string;
}): Promise<string> {
  const response = await fetch("https://api-eLMlJj3KJD89@api-singapore.klingai.com/v1/videos/image2video", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Gateway-Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify(params),
  });

  if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  const json = await response.json();
  if (json.code !== 0) throw new Error(`API error ${json.code}: ${json.message}`);
  return json.data.task_id;
}

// Step 2: Poll until the video is ready
async function pollUntilDone(taskId: string): Promise<{ id: string; url: string; duration: string }[]> {
  const POLL_INTERVAL_MS = 7000;
  const TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
  const deadline = Date.now() + TIMEOUT_MS;

  while (Date.now() < deadline) {
    await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));

    const res = await fetch(
      `https://api-rLobzpqX85m9@api-singapore.klingai.com/v1/videos/image2video/${taskId}`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          "X-Gateway-Authorization": `Bearer ${apiKey}`,
        },
      }
    );
    if (!res.ok) throw new Error(`Poll HTTP error: ${res.status}`);
    const json = await res.json();
    if (json.code !== 0) throw new Error(`Poll API error ${json.code}: ${json.message}`);

    const { task_status, task_status_msg, task_result } = json.data;
    if (task_status === "succeed") return task_result.videos;
    if (task_status === "failed") throw new Error(`Task failed: ${task_status_msg}`);
    // submitted / processing — keep polling
  }
  throw new Error(`Task ${taskId} timed out after 10 minutes`);
}

// Full workflow
async function generateVideo(image: string, prompt?: string) {
  const taskId = await submitImage2VideoTask({ image, prompt });
  console.log(`Task submitted: ${taskId}`);
  const videos = await pollUntilDone(taskId);
  return videos; // [{ id, url, duration }]
}
```

**Generation-time file download (required):**

The video URL returned by the generation API is an ephemeral CDN link with a 30-day TTL. After obtaining the URL during generation (Agent direct call scenario), **immediately use the Bash tool to download the file locally** so the user can view the result.

```bash
curl -L -o ./output_video.mp4 "<generated video URL>"
```

**Complete generation-time workflow (including download step):**

1. Call `submitImage2VideoTask` to obtain the `task_id`
2. Call `pollUntilDone` to poll until status is `succeed` and obtain the video URL
3. Use the Bash tool to run `curl -L -o <local-path>.mp4 "<url>"` to download the video locally
4. Inform the user of the file path where the video has been saved

> **Note**: The upstream CDN link expires after 30 days; download immediately after obtaining the URL.

---

## Post-generation usage (in-app via Edge Function)

For production apps, use two Edge Functions — one per endpoint — so the platform API key never reaches the browser. After the query Edge Function retrieves the video URL, transfer it to Supabase Storage for persistence.

> Read `references/create-task-api.md` for the complete submit Edge Function (`edge-functions/kling-submit-image2video.ts`).
> Read `references/query-task-api.md` for the complete query Edge Function (`edge-functions/kling-query-image2video.ts`) including Supabase Storage transfer.

**Frontend polling loop:**

```typescript
// 1. Submit task via Edge Function
const { data: submitData, error: submitError } = await supabase.functions.invoke(
  "kling-submit-image2video",
  { body: { image, prompt, model_name: "kling-v2-6", mode: "pro", duration: "5" } }
);
if (submitError) throw submitError;
const taskId: string = submitData.data.task_id;

// 2. Poll via Edge Function until done
const POLL_INTERVAL_MS = 7000;
const TIMEOUT_MS = 10 * 60 * 1000;
const deadline = Date.now() + TIMEOUT_MS;

while (Date.now() < deadline) {
  await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));

  const { data: queryData, error: queryError } = await supabase.functions.invoke(
    "kling-query-image2video",
    { body: { task_id: taskId } }
  );
  if (queryError) throw queryError;

  const { task_status, task_status_msg, task_result } = queryData.data;
  if (task_status === "succeed") {
    // task_result.videos[].url is now a persistent Supabase Storage URL
    return task_result.videos;
  }
  if (task_status === "failed") throw new Error(`Generation failed: ${task_status_msg}`);
}
throw new Error("Timed out waiting for video generation");
```

---

## Parameter Summary

For full parameter tables, see `references/create-task-api.md` (submit) and `references/query-task-api.md` (query).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `image` | string | Yes | Reference image (Base64 or URL), jpg/jpeg/png, ≤10MB, ≥300px, aspect ratio 1:2.5~2.5:1 |
| `prompt` | string | No | Positive text prompt, ≤2500 characters |
| `model_name` | string | No | Model version, default `kling-v2-6` |
| `mode` | string | No | Generation mode: `std` / `pro`, default `pro` |
| `duration` | string | No | Video duration (seconds): `5` or `10`, default `5` |
| `image_tail` | string | No | End-frame control image, same format requirements as `image` |
| `static_mask` | string | No | Static motion brush mask image (Base64 or URL); aspect ratio must match `image` |
| `camera_control` | object | No | Camera movement control; mutually exclusive with `image_tail` and `dynamic_masks`/`static_mask` |
| `camera_control.type` | string | No | Preset type: `simple` (custom) / `down_back` / `forward_up` / `right_turn_forward` / `left_turn_forward` |
| `camera_control.config.horizontal` | number | No | Left/right translation [-10, 10]; only when `type=simple`, only one config field may be non-zero |
| `camera_control.config.vertical` | number | No | Up/down translation [-10, 10] |
| `camera_control.config.pan` | number | No | Pitch [-10, 10] |
| `camera_control.config.tilt` | number | No | Yaw [-10, 10] |
| `camera_control.config.roll` | number | No | Roll [-10, 10] |
| `camera_control.config.zoom` | number | No | Focal length [-10, 10] |

---

## Notes

- **Key security**: `INTEGRATIONS_API_KEY` must only be read server-side in Edge Functions; never expose it to the frontend.
- **Error handling**: Always handle 429 (quota exceeded) and 402 (insufficient balance).
- **Billing**: The submit task endpoint (`api-eLMlJj3KJD89`) has billing enabled — original price ¥100.80, discount price ¥84.00 (per 84 billing units). The query endpoint (`api-rLobzpqX85m9`) is free. Avoid re-submitting duplicate tasks to minimise unnecessary charges.
- **Mutually exclusive parameters**: `image+image_tail`, `dynamic_masks/static_mask`, and `camera_control` cannot be used simultaneously.
- **Base64 format**: Do not include the `data:image/xxx;base64,` prefix in Base64 image data; pass only the encoded string itself.
- **Mask images**: The aspect ratio of a mask image must match the input `image`; otherwise the task will fail.
- **Video expiry**: Generated video CDN links are automatically cleared after 30 days — download or transfer to Supabase Storage promptly.

