Kling 3 API: First and Last Frames to a Saved Video

For a two-image Kling workflow, assign one URL to the first frame and the other to the last frame, submit the request once, and keep the resulting task identity separate from the eventual video file. A failed download should take you back to the saved task result—not back to generating the video again.
This tutorial uses Tokenhot's kling-v3 route, not the direct Kling API or kling-v3-omni. The first/last-frame reference documents the frame roles and request format. One checkpoint remains manual: its submission response example is {}, so this guide does not invent the JSON path containing the new task ID. You must confirm that ID from your actual response or provider records before continuing.
The companion was checked with synthetic offline responses. No real two-frame generation, video-quality assessment or account charge was produced for this article.
Prepare the two images before making a generation request
Choose a start image and an end image that describe the same intended sequence. As an editorial example, the first could show a closed product box on a table and the last the same box open. Keep the subject and viewpoint consistent enough that your motion prompt describes a plausible transition. This is a prompt-design suggestion, not a guarantee of frame-perfect output.
Host images at URLs that the API can retrieve without your browser session. The helper expects trusted HTTPS URLs. Test accessibility from an appropriate environment and check that each URL returns the intended image rather than an HTML login page. This tutorial does not upload files or infer a server-side file limit from another Kling mode.
The documented payload uses file_infos. Its nested field capitalization matters:
{
"model": "kling-v3",
"prompt": "Starting from <<<image_1>>>, the box lid slowly opens. Keep the camera fixed and finish in the pose shown by the last frame.",
"duration": 5,
"size": "720P",
"file_infos": [
{"Type": "Url", "Category": "Image", "Url": "https://your-media-host.example/first.jpg", "Usage": "FirstFrame"},
{"Type": "Url", "Category": "Image", "Url": "https://your-media-host.example/last.jpg", "Usage": "LastFrame"}
],
"audio_generation": false
}
Replace both illustrative URLs. The reference explains the optional last-frame role; the two-entry request above is an authored extension of its one-frame example. The selected duration and resolution follow that example, not an exhaustive capability table. Do not assume settings copied from text-only generation are interchangeable.
Prepare and submit with the companion script
Save kling_frames.py locally. It uses Python's standard library; the local checks were run on Python 3.13.5. Set FIRST_FRAME_URL and LAST_FRAME_URL to your real media locations, then prepare the request without contacting an API:
python kling_frames.py prepare \
--first "$FIRST_FRAME_URL" \
--last "$LAST_FRAME_URL" \
--prompt 'Starting from <<<image_1>>>, slowly open the box, keep the camera fixed, and finish at the last-frame pose.' \
--output frames-request.json
Inspect the generated JSON. Then, only with authorization to use your account, set TOKENHOT_API_KEY in your environment and run:
python kling_frames.py submit \
--request frames-request.json \
--run runs/kling-submit-001 \
--execute
This sends one JSON POST to https://api.tokenhot.ai/v1/video/generations. The program reserves a new run directory before sending, keeps the request locally, and saves the response before interpreting it. It does not follow redirects or retry a POST.
A successful HTTP submission is not a finished video. Inspect runs/kling-submit-001/response.json privately and identify the task ID associated with that submission. The helper deliberately does not guess id, task_id or data.task_id. If the response does not make the identifier clear, stop and obtain the route's submission contract from Tokenhot support. Do not send another generation merely to obtain a better-looking response.
If a transport failure occurs, the run's metadata records the observed HTTP status when available. Absence of a usable response leaves the POST outcome uncertain. The existence of a fresh output name does not make repeating that request safe: reconcile the earlier attempt first.
Continue from the confirmed task ID
The task-query reference documents a Bearer-authenticated GET at /v1/video/generations/{task_id}. Its task state is data.status: IN_PROGRESS, SUCCESS or FAILURE. The helper uses that field rather than an inner upstream status or a percentage.
Set KLING_TASK_ID to the ID you confirmed, then start a separate query run:
python kling_frames.py poll \
--task-id "$KLING_TASK_ID" \
--run runs/kling-query-001 \
--attempts 12 \
--interval 10 \
--execute
Each query gets its own saved JSON and metadata. A SUCCESS result also becomes result.json. A failure or unexpected status stops the loop. The attempt count and interval are client choices, not promised generation times or provider rate-limit guidance.
If the polling budget ends while the task is still processing, reuse the same task ID in a new query directory. Do not rerun submit. If the task reports failure, inspect the preserved response and decide whether a corrected new request is justified; there is no automatic resubmission branch.
Save the video without forwarding your API key
The query documentation distinguishes a gateway preview URL at data.result_url from the direct MP4 location at data.data.metadata.url. This helper uses the direct location for download and attaches no API Authorization header to it. It does not assume the preview proxy's access rules.
For a successful saved result:
python kling_frames.py save \
--response runs/kling-query-001/result.json \
--output output/box-opening.mp4 \
--execute
The direct URL in the documentation's sample uses HTTP. The helper defaults to HTTPS and refuses that downgrade. If your actual provider-returned link is HTTP, decide whether that unencrypted transfer is acceptable for the content and environment before explicitly adding --allow-http. Do not silently rewrite it to HTTPS or send a credential to make it work. A redirected or expired link needs provider-specific investigation, not another generation by default.
The downloader checks an MP4 file signature and refuses existing output paths. That is a lightweight format check, not proof the entire video decodes. It also caps an in-memory download at 256 MiB; this is the example's local memory guard, not a Kling file-size limit. For large files, adapt the downloader to bounded streaming and keep its no-overwrite and no-credential behavior.
Accept the output as a video, not just a task state
Play the saved file before treating it as a usable asset. Check that the opening and closing frames are appropriate, the motion matches your brief, there are no unacceptable subject changes, and the clip is suitable for its intended use. Retain the prompt and exact image versions so an intentional revision is distinguishable from an accidental duplicate.
Keep raw responses and media URLs private: a signed URL may itself grant access to an asset. The supplied downloader is a local CLI for trusted URLs, not a hardened public URL-fetch service or an SSRF firewall.
This guide's deliverable is the route-specific two-frame request and a recoverable client sequence. Live success, visual fidelity, URL lifetime and cost still require observations from your authorized route. For the broader question of moving an existing video application, use the separate video migration guide.
Build a two-frame Kling 3 request, preserve the submission, query a confirmed task ID, and download the result without repeating video generation.


