Kolbo.AIKolbo.AI Docs
Developer API

First & Last Frame

Generate videos by interpolating between a first and last frame image using the Kolbo API.

Generate videos by providing a first and last frame image. The AI interpolates between the two frames, creating smooth animated transitions.

Smart Select: Omit model (or send "auto" / "smart-select") and Kolbo routes the request to a model itself. Passing a specific identifier from GET /api/v1/models?type=first_last_frame gives you deterministic behaviour and lets you pre-validate its duration and aspect-ratio support.

Model identifiers are Kolbo-specific. Never hardcode model identifiers — always fetch the current list from GET /api/v1/models?type=first_last_frame first. Models may be added, renamed, or retired at any time.

All generation endpoints accept an optional project_id body field that routes the output into a specific project. See Projects.

Endpoint

POST /api/v1/generate/first-last-frame

type=first_last_frame on the models endpoint is an alias for the underlying model type firstlastgenerations; both work.

Rate limit: 10 requests per minute per user, counted in a bucket shared with the other standard-limit /v1/generate/* endpoints. Image generation and transcription sit in their own, higher-limit buckets.

Request Body

Accepts application/json or multipart/form-data (when uploading files).

You must provide exactly two frames, in one of two ways — do not mix them:

  • URL mode: Provide first_frame_url and last_frame_url as JSON fields.
  • File mode: Upload exactly 2 image files via the multipart files field (first frame, then last frame). Maximum 10 MB per file, 2 files per request.
FieldTypeRequiredDescription
first_frame_urlstringConditionalURL of the first frame image (required in URL mode)
last_frame_urlstringConditionalURL of the last frame image (required in URL mode)
filesmultipartConditionalExactly 2 image files: first frame then last frame (required in file mode). Repeat the files field once per frame, first frame first. Max 10 MB per file. Anything other than exactly 2 files (with no frame URLs) is rejected with a 400; a third file is rejected by the upload layer before the handler runs.
promptstringNoDescribes the motion between the frames. Optional, but must be a string when present (400 INVALID_PROMPT otherwise). Defaults to an empty prompt.
modelstringNoModel identifier from GET /api/v1/models?type=first_last_frame. Must be a string, never an array (400 INVALID_MODEL). Omitted / "auto" / "smart-select" → Smart Select.
durationnumber | stringNoDuration in seconds. Default 5. Not range-checked by this endpoint — an unsupported value is coerced or rejected by the provider. Read supported_durations / default_duration off the chosen model first.
aspect_ratiostringNoDefault "16:9". The SDK always sends a value, so the auto-detect-from-first-frame path never runs here — pass the ratio explicitly if you do not want 16:9. Read supported_aspect_ratios (or supported_aspect_ratios_by_type) off the chosen model.
enhance_promptbooleanNoRewrite the prompt with the prompt enhancer. Default true (send false to disable).
visual_dna_idsarray of stringsNoVisual DNA ids. Every id is access-checked before the generation starts — one you do not own or have shared access to fails the request with 400 Visual DNA not found or inaccessible. Cap: max_visual_dna; ignored when supports_visual_dna is false.
resolutionstringNoVideo resolution tier. Model-dependent — check supported_resolutions. On Kling-family identifiers the value also selects the matching model variant, and a tier that family does not publish is rejected with 400 UNSUPPORTED_KLING_RESOLUTION. Higher tiers may multiply the credit cost — see Credit Multipliers.
sound_enabledbooleanNoRequest synced audio on models that can emit it. Only meaningful when sound_generation_type is not "none"; sound_enabled_by_default is the model default and sound_credit_multiplier is the cost penalty when on.
project_idstringNoTarget project. Omit to use your auto-created "API Generations" project.

Do not mix URLs and files. Provide either both first_frame_url and last_frame_url, OR exactly two multipart files. Sending frame URLs together with uploaded files returns a 400.

Examples

URL Mode

cURL (Smart Select):

curl -X POST https://api.kolbo.ai/api/v1/generate/first-last-frame \
  -H "X-API-Key: kolbo_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "first_frame_url": "https://example.com/sunrise.jpg",
    "last_frame_url": "https://example.com/sunset.jpg",
    "prompt": "Smooth time-lapse transition from sunrise to sunset over a cityscape",
    "duration": 5,
    "aspect_ratio": "16:9"
  }'

JavaScript:

const API_KEY = "kolbo_live_YOUR_API_KEY";

async function main() {
  // Optional: fetch available models
  // const models = await fetch("https://api.kolbo.ai/api/v1/models?type=first_last_frame", {
  //   headers: { "X-API-Key": API_KEY }
  // }).then(r => r.json());
  // console.log("Available models:", models);

  const response = await fetch("https://api.kolbo.ai/api/v1/generate/first-last-frame", {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      first_frame_url: "https://example.com/sunrise.jpg",
      last_frame_url: "https://example.com/sunset.jpg",
      prompt: "Smooth time-lapse transition from sunrise to sunset over a cityscape",
      duration: 5,
      aspect_ratio: "16:9"
    })
  });

  const started = await response.json();
  if (!started.success) throw new Error(started.error);

  const result = await pollUntilDone(started.generation_id, started.poll_interval_hint);
  if (result.state !== "completed") throw new Error(result.error || result.state);

  console.log("Video URL:", result.result.urls[0]); // urls is always an array
}

const TERMINAL = new Set(["completed", "failed", "cancelled"]);

// Minimal loop. A production client should also handle transient HTTP errors and
// an overall timeout — see /docs/developer-api/polling-and-cancellation.
async function pollUntilDone(generationId, intervalSeconds) {
  while (true) {
    await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
    const status = await fetch(
      `https://api.kolbo.ai/api/v1/generate/${generationId}/status`,
      { headers: { "X-API-Key": API_KEY } }
    ).then((r) => r.json());
    if (TERMINAL.has(status.state)) return status;
  }
}

main();

Python:

import requests
import time

API_KEY = "kolbo_live_YOUR_API_KEY"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Optional: fetch available models
# models = requests.get(
#     "https://api.kolbo.ai/api/v1/models?type=first_last_frame",
#     headers={"X-API-Key": API_KEY},
# ).json()
# print("Available models:", models)

response = requests.post(
    "https://api.kolbo.ai/api/v1/generate/first-last-frame",
    headers=HEADERS,
    json={
        "first_frame_url": "https://example.com/sunrise.jpg",
        "last_frame_url": "https://example.com/sunset.jpg",
        "prompt": "Smooth time-lapse transition from sunrise to sunset over a cityscape",
        "duration": 5,
        "aspect_ratio": "16:9",
    },
)

started = response.json()

TERMINAL = {"completed", "failed", "cancelled"}


def poll_until_done(generation_id, interval_seconds):
    """Minimal loop. Add transient-error retries and an overall timeout for
    production — see /docs/developer-api/polling-and-cancellation."""
    while True:
        time.sleep(interval_seconds)
        status = requests.get(
            f"https://api.kolbo.ai/api/v1/generate/{generation_id}/status",
            headers={"X-API-Key": API_KEY},
        ).json()
        if status["state"] in TERMINAL:
            return status


result = poll_until_done(started["generation_id"], started["poll_interval_hint"])
if result["state"] != "completed":
    raise Exception(result.get("error", result["state"]))

print("Video URL:", result["result"]["urls"][0])  # urls is always a list

File Mode

cURL with file uploads:

curl -X POST https://api.kolbo.ai/api/v1/generate/first-last-frame \
  -H "X-API-Key: kolbo_live_YOUR_API_KEY" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "prompt=Smooth transition between the two frames" \
  -F "duration=5" \
  -F "aspect_ratio=16:9"

Python with file uploads:

import requests

API_KEY = "kolbo_live_YOUR_API_KEY"

files = [
    ("files", ("first-frame.jpg", open("first-frame.jpg", "rb"), "image/jpeg")),
    ("files", ("last-frame.jpg", open("last-frame.jpg", "rb"), "image/jpeg")),
]

started = requests.post(
    "https://api.kolbo.ai/api/v1/generate/first-last-frame",
    headers={"X-API-Key": API_KEY},
    files=files,
    data={
        "prompt": "Smooth transition between the two frames",
        "duration": "5",
        "aspect_ratio": "16:9",
    },
).json()

# poll_until_done() from the URL-mode example above — file mode returns the same
# envelope and is polled the same way.
result = poll_until_done(started["generation_id"], started["poll_interval_hint"])
print("Video URL:", result["result"]["urls"][0])

With Specific Model

First, fetch available models:

curl https://api.kolbo.ai/api/v1/models?type=first_last_frame \
  -H "X-API-Key: kolbo_live_YOUR_API_KEY"

Then use a model identifier from the response:

curl -X POST https://api.kolbo.ai/api/v1/generate/first-last-frame \
  -H "X-API-Key: kolbo_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "first_frame_url": "https://example.com/sunrise.jpg",
    "last_frame_url": "https://example.com/sunset.jpg",
    "prompt": "Smooth time-lapse transition from sunrise to sunset",
    "model": "MODEL_IDENTIFIER_FROM_MODELS_ENDPOINT",
    "duration": 5
  }'

Model identifiers come from GET /api/v1/models?type=first_last_frame. Always fetch the latest list rather than hardcoding identifiers, as models may change over time.

Response

This endpoint is asynchronous and fire-and-forget: the POST answers 202 Accepted as soon as the job is queued and never contains a video.

Polling is the only completion mechanism. Kolbo never calls you back: there is no webhook, no callback_url field on this request body, and no route delivers outbound notifications. Socket.IO events are the web app's internal transport and are not part of the API contract — API-key generations are registered so the shared progress emitter drops their events, and the few emit sites that bypass that check are undocumented, unversioned and unsafe to build on. After the POST, loop against GET /api/v1/generate/{generation_id}/status until state is completed, failed or cancelled. Full contract: Polling & Cancellation.

Generation Started

{
  "success": true,
  "generation_id": "6820f1c2a1b2c3d4e5f60718",
  "type": "first_last_frame",
  "model": "auto",
  "credits_charged": null,
  "poll_url": "/v1/generate/6820f1c2a1b2c3d4e5f60718/status",
  "poll_interval_hint": 8,
  "session_id": "6820f1c2a1b2c3d4e5f60700",
  "project_id": "6820f1c2a1b2c3d4e5f606ff"
}
FieldTypeNotes
generation_idstringMongo ObjectId. This is what you poll and cancel with.
typestringAlways "first_last_frame"
modelstringEchoes "auto" for Smart Select requests, otherwise the identifier you sent
credits_chargednullAlways null on this endpoint — the pipeline reports no estimate at submit time. Read credits_used from the completed status.
poll_urlstringThe status path without the /api prefix. Prepend https://api.kolbo.ai/api.
poll_interval_hintnumberSuggested seconds between polls — 8 here
session_id / project_idstringWhere the generation lives in the Kolbo app

Completed Status

GET /api/v1/generate/{generation_id}/status

{
  "success": true,
  "generation_id": "6820f1c2a1b2c3d4e5f60718",
  "type": "first_last_frame",
  "state": "completed",
  "progress": 100,
  "result": {
    "urls": ["https://media.kolbo.ai/videos/.../output.mp4"],
    "thumbnail_url": "https://media.kolbo.ai/videos/.../thumb.jpg",
    "duration": 5,
    "aspect_ratio": "16:9",
    "prompt_used": "Smooth time-lapse transition from sunrise to sunset over a cityscape",
    "model": "MODEL_IDENTIFIER",
    "created_at": "2026-04-12T10:30:00.000Z"
  },
  "credits_used": 75,
  "credits_breakdown": [
    { "model": "MODEL_IDENTIFIER", "amount": 75, "base": 50, "final": 75, "duration_multiplier": 1.5, "pricing": null }
  ]
}

The output lives in result.urls — always an array of strings, even though this endpoint produces exactly one video. Read urls[0].

result (and credits_used / credits_breakdown) appear only under state: "completed"; a processing body has exactly five keys and no result at all. There is no partial read.

result.thumbnail_url falls back to the first frame you supplied when the pipeline produced no poster. result.duration is a number when the pipeline measured it and a string when it falls back to the requested duration — coerce it. result.model is never "auto": it resolves to the identifier that actually ran. The visual_dna, preset and cinematic_presets keys are added only when you sent those inputs. credits_used is the authoritative, multiplier-adjusted cost.

Failure and cancellation

A failed generation is still an HTTP 200 with success: true — the failure is in state, with an error string and a best-effort failure object. cancelled carries neither result nor error, so treat all three terminal states explicitly. Failed generations are not charged. See Polling & Cancellation.

Errors

StatusCodeCause
400Neither both frame URLs nor exactly two files were provided, or URLs and files were mixed
400INVALID_PROMPTprompt sent as a non-string
400INVALID_MODELmodel sent as an array or object instead of a string, or the identifier does not resolve to a visible first-last-frame model
400UNSUPPORTED_KLING_RESOLUTIONresolution is not one of the tiers the chosen Kling model family publishes
400GENERATION_ERRORA visual_dna_ids entry does not exist or you do not have access to it
403INSUFFICIENT_CREDITSBalance too low — checked before the generation starts
429More than 10 generation requests in one minute

Tips

  • First & Last Frame generation typically takes 1-5 minutes depending on the model and duration.
  • Both frames should have the same aspect ratio for best results.
  • The prompt field is optional but recommended — it guides the interpolation style (e.g., "smooth camera pan", "dramatic zoom", "time-lapse").
  • File uploads are limited to 10 MB per file, two files per request.
  • Credit cost depends on the model, duration, resolution tier, and whether sound is enabled — see Models and Pricing.
  • Use poll_interval_hint from the initial response to set your polling interval.
  • Check supported_durations, supported_aspect_ratios, and supported_resolutions on each model via the Models endpoint before requesting specific values — this endpoint does not validate them for you, so an unsupported value reaches the provider.