Kling 3.0 is one of the stronger image-to-video models available right now, with flexible duration, resolution modes up to 4K, built-in audio generation, and end-frame control for guiding how a clip finishes. This tutorial walks through building a real Python integration for it, from authentication through a working FastAPI endpoint you can drop into a backend. If you want the feature and pricing overview first, start with our Kling 3.0 API guide, or see Kling 2.1 to 3.0 if you are still deciding which version to use.
We are using Apiframe as the API layer here, since it exposes Kling 3.0 through a single unified endpoint alongside every other major video model, with one API key and one credit balance instead of a separate integration per provider.
What You'll Build
By the end of this tutorial you will have a Python script that sends a source image and a text prompt to Kling, polls until the video is ready, and returns a finished video URL. We will then wrap that logic in a minimal FastAPI endpoint so it can be called from a real application.
Prerequisites
You will need Python 3.10 or later, an Apiframe API key (sign up at console.apiframe.ai and generate one from the dashboard), and the requests library. httpx works as a drop-in alternative if you prefer an async client, meaning one that can handle other work while waiting on a response. Getting Started with Apiframe covers signup and key creation if this is your first time. Install what you need with:
pip install requests fastapi uvicornAuthenticating Your Requests
Every request to the Apiframe API needs an X-API-Key header. API keys always start with the prefix afk_. Before writing the real integration, it is worth running a quick check to confirm your key works:
import requests
API_KEY = "afk_your_api_key_here"
response = requests.get(
"https://api.apiframe.ai/v2/me",
headers={"X-API-Key": API_KEY},
)
print(response.json())If your key is valid, this returns your account details along with your team's current credit balance. A 401 here means the key is missing, invalid, or has been revoked, in which case double-check what you copied from the dashboard.
Sending Your First Image-to-Video Request
Kling's image-to-video generation runs through the same endpoint as text-to-video: POST /v2/videos/generate, with model set to "kling-3.0". What makes it image-to-video is passing a start_image URL inside klingParams, that is, the image Kling will animate as the first frame.
Required parameters
prompt: the text description of what should happen in the video (1 to 4,000 characters)model:"kling-3.0"klingParams.start_image: a URL pointing to the source image you want animated
Optional parameters
klingParams.duration: video length in seconds, from 3 to 15 (defaults to 5)klingParams.mode:"standard"(720p),"pro"(1080p, the default), or"4k"(2160p)klingParams.aspect_ratio: aspect ratio as a"W:H"string, like"16:9"klingParams.end_image: a URL for the image Kling should end on, useful for controlled transitionsklingParams.negative_prompt: things to avoid in the output (up to 2,500 characters)klingParams.generate_audio: whether to generate synced audio (defaults totrue). Kling is one of a handful of models that produce audio and video together.klingParams.multi_prompt: a JSON array of timed prompts, passed as a string, if you need timeline-level control over the clip
Here is a full working request that animates a source image with a 10-second prompt:
import requests
API_KEY = "afk_your_api_key_here"
response = requests.post(
"https://api.apiframe.ai/v2/videos/generate",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={
"prompt": "the camera slowly pushes in as the subject turns toward the light",
"model": "kling-3.0",
"klingParams": {
"start_image": "https://example.com/your-source-image.jpg",
"duration": 10,
"mode": "pro",
"aspect_ratio": "16:9",
"generate_audio": True,
},
},
)
job = response.json()
print(job) # {"jobId": "...", "status": "QUEUED"}The request returns immediately, before the video actually exists. Apiframe queues the job and hands you a jobId, which you use to check on progress.
Polling for the Generated Video
Video generation always runs as a background job, Kling included, so the pattern is the same everywhere: submit the request, then either poll for the result or wait for a webhook. Apiframe recommends polling every 5 to 10 seconds for video jobs, since they take noticeably longer than image generations. The same submit-and-poll loop applies to every video model on the platform, which our general image-to-video tutorial walks through if you want to compare approaches.
import time
import requests
API_KEY = "afk_your_api_key_here"
def wait_for_video(job_id: str, timeout_seconds: int = 600, poll_interval: int = 8) -> str:
headers = {"X-API-Key": API_KEY}
deadline = time.time() + timeout_seconds
while time.time() < deadline:
response = requests.get(
f"https://api.apiframe.ai/v2/jobs/{job_id}",
headers=headers,
)
job = response.json()
status = job["status"]
if status == "COMPLETED":
return job["result"]["videoUrl"]
elif status == "FAILED":
raise RuntimeError(f"Generation failed: {job['error']}")
print(f"Status: {status} ({job['progress']}%)")
time.sleep(poll_interval)
raise TimeoutError(f"Job {job_id} did not complete within {timeout_seconds} seconds")A COMPLETED job returns a result object shaped like {"videoUrl": "https://cdn2.apiframe.ai/videos/....mp4"}. That file stays on Apiframe's CDN for 90 days, so download it to your own storage if you need it longer term.
If you would rather not poll at all, pass a webhookUrl and webhookEvents (["completed", "failed"]) in the original generation request and Apiframe will POST the result to your server when the job finishes. That is generally the better option for production traffic, since it avoids holding a connection or a background loop open per job. Our walkthrough on setting up webhook callbacks covers receiving and verifying those requests.
Handling Errors and Retries
The error format is consistent across the API: a JSON body with an error message and a details object for field-level validation issues. The status codes worth handling explicitly:
| Status | Meaning | What to do |
|---|---|---|
| 400 | Validation failed, check details | Fix the request, do not retry as-is |
| 401 | Missing or invalid API key | Check your X-API-Key header |
| 402 | Insufficient credits | Top up before retrying |
| 403 | Forbidden, key inactive or admin required | Check key status in the console |
| 404 | Resource not found | Verify the job ID |
| 429 | Rate limit exceeded | Back off and retry |
| 503 | Service temporarily unavailable | Retry with exponential backoff |
A job can also come back with status: "FAILED" after being accepted, most often from a content-moderation rejection on the prompt or source image. The good news: credits are refunded automatically whenever a job fails, so a failed generation does not cost you anything. Our guide to content moderation for generated media explains what usually trips these filters and how to design around them.
For retry logic, avoid blindly resubmitting on every failure. Content-policy failures will fail again with the same input, so retrying only helps for temporary errors like 429 or 503. If you are worried about accidentally submitting the same job twice during a retry, pass an Idempotency-Key header. Sending the same key again returns the original job instead of creating a duplicate and charging you twice.
import uuid
import requests
def submit_with_idempotency(payload: dict, api_key: str) -> dict:
idempotency_key = str(uuid.uuid4())
response = requests.post(
"https://api.apiframe.ai/v2/videos/generate",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json=payload,
)
return response.json()Wrapping It in a FastAPI Endpoint
Putting the pieces together, here is a minimal FastAPI endpoint that accepts an image URL and a prompt, submits the Kling job, waits for it to complete, and returns the finished video URL:
import os
import time
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
API_KEY = os.environ["APIFRAME_API_KEY"]
BASE_URL = "https://api.apiframe.ai/v2"
class ImageToVideoRequest(BaseModel):
image_url: str
prompt: str
duration: int = 5
mode: str = "pro"
@app.post("/generate-video")
def generate_video(req: ImageToVideoRequest):
submit_response = requests.post(
f"{BASE_URL}/videos/generate",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={
"prompt": req.prompt,
"model": "kling-3.0",
"klingParams": {
"start_image": req.image_url,
"duration": req.duration,
"mode": req.mode,
},
},
)
if submit_response.status_code != 202:
raise HTTPException(status_code=submit_response.status_code, detail=submit_response.json())
job_id = submit_response.json()["jobId"]
deadline = time.time() + 600
while time.time() < deadline:
job = requests.get(f"{BASE_URL}/jobs/{job_id}", headers={"X-API-Key": API_KEY}).json()
if job["status"] == "COMPLETED":
return {"videoUrl": job["result"]["videoUrl"], "jobId": job_id}
elif job["status"] == "FAILED":
raise HTTPException(status_code=500, detail=job["error"])
time.sleep(8)
raise HTTPException(status_code=504, detail="Generation timed out")For anything beyond a demo, swap the blocking while loop, which ties up the request for the whole generation, for a webhook callback or a background task queue (Celery, arq, or FastAPI's own BackgroundTasks), so the endpoint returns the jobId immediately instead of holding the HTTP connection open for the full generation time.
Kling Pricing Recap for This Workflow
Kling 3.0 through Apiframe bills per second of output, and the rate depends on the mode and whether audio is switched on. Standard mode is 11 credits per second (16 with audio), pro mode is 14 credits per second (21 with audio), and 4K is 26 credits per second whether or not you generate audio. At one cent per credit, that puts a 5-second pro clip with audio at 105 credits ($1.05), while a 10-second standard clip without audio comes to 110 credits ($1.10), so twice the length at lower resolution lands at roughly the same spend. Budget accordingly if you are planning to run this at volume, and check Apiframe's pricing page for current rates, or read our full breakdown in the AI video API pricing guide if you are comparing Kling against Runway, Hailuo, or Seedance for this use case. If you need to forecast a monthly bill, our post on estimating AI video costs works through the math.
FAQ
Does Kling have an official public API?
Yes, Kling AI runs its own developer platform with prepaid resource packages. Going through Apiframe instead gets you the same model behind a single unified endpoint alongside every other major video and image model, so you are not managing a separate account and credit system just for Kling. If you are weighing your options, we compare the main Kling 3.0 API providers side by side.
Which Python version and libraries does this need?
Python 3.10 or newer, plus requests (or httpx for an async client). The FastAPI example above also needs fastapi and uvicorn to run as a server.
How long does a typical generation take?
Video jobs on Apiframe generally take one to five minutes depending on duration and mode, with 4K and longer clips at the slower end of that range. That is why polling every 5 to 10 seconds, or using a webhook instead, works better than a tight polling loop. Our latency guide breaks down what actually drives wait times across models.