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. An identifier Kolbo does not recognise is rejected with 400 MODEL_NOT_FOUND. 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: 30 accepted requests per minute per user, counted in one bucket shared with the other standard-limit routes (among them video, image-to-video, elements, lipsync, music, speech, sound, Creative Director, chat, media upload and generation cancel). Requests that fail are not counted. Image generation (30/min) and transcription (100/min) have their own 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.

There is no multi-reference input on this endpoint: it takes exactly one first frame and one last frame. For several reference images, videos or audio use Elements.

Frame URLs must be public http(s) URLs — private, internal or non-HTTP addresses are rejected with 400 INVALID_IMAGE_URL. Each frame is downloaded and re-encoded server-side before it reaches the model.

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. The field must be named files. Anything other than exactly 2 files (with no frame URLs) is rejected with a 400; a third file, an oversized file or a differently named file field 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). An empty prompt is sent to the model as "Generate Video". @Name mentions of your Visual DNAs are resolved (see visual_dna_ids); #Name moodboard mentions are stripped, since this tool applies no moodboards.
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. An unrecognised identifier returns 400 MODEL_NOT_FOUND; a recognised but hidden (retired) identifier is rejected with 400 INVALID_MODEL. Models that run only in Elements or Video to Video are rejected with 400 MODEL_NOT_SUPPORTED_ON_THIS_ENDPOINT.
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" when omitted. Send "auto" to match the first frame's own aspect ratio (snapped to a ratio the model supports). Read supported_aspect_ratios (or supported_aspect_ratios_by_type) off the chosen model.
enhance_promptbooleanNoRewrite the prompt with the prompt enhancer. Default false. true enables it — in a multipart request send the string "true" (or "1").
visual_dna_idsarray of stringsNoVisual DNA ids. Because you always supply both frames, a DNA here is a character/identity reference and never replaces a frame. Every id is access-checked before the generation starts — one you do not own or have shared access to fails the request with 400 GENERATION_ERROR ("Visual DNA not found or inaccessible"). In multipart send a JSON-encoded array string ('["id1","id2"]'); any other string is silently ignored. When omitted, @Name mentions in prompt are resolved to your DNAs on models that support Visual DNA. This endpoint does not enforce a count — stay within the model's max_visual_dna, and expect no effect 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. A named draft tier ("480p-draft", listed in supported_resolutions / draft_resolutions of the models that offer it) needs an explicit model that publishes it, otherwise 400 UNSUPPORTED_VIDEO_RESOLUTION. Higher tiers may multiply the credit cost — see Credit Multipliers.
draftbooleanNoBoolean alias for the draft tier. true forces the model's draft resolution (overriding resolution) and is accepted only for models that publish one in draft_resolutions — otherwise 400 "Draft is not supported by the selected model." false maps a -draft tier back to its regular resolution. Any non-boolean value (other than the strings "true" / "false") is a 400.
sound_enabledbooleanNoRequest synced audio on models that can emit it. Omit to use the model's sound_enabled_by_default. Only meaningful when sound_generation_type is not "none"; sound_credit_multiplier is the cost penalty when on. In multipart send the strings "true" / "false" ("0" also means off); omit the field to get the model default.
project_idstringNoTarget project. Omit to use your auto-created "API Generations" project. See Projects.
session_idstringNoAdd the generation to an existing video session — one returned by an earlier call to this endpoint, or to video, image-to-video or Elements, which share the same session type. Wins over project_id — the generation lands in that session's project. A malformed id is 400 SDK_SESSION_INVALID_ID; an unknown id or a session of a different kind (for example an image or music session) is 404 SDK_SESSION_NOT_FOUND.

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"
modelstring"auto" for Smart Select requests, otherwise the canonical identifier of the model 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.prompt_used is the enhanced prompt when the enhancer ran, otherwise your prompt. result.model is never "auto": it resolves to the identifier that actually ran (null only if none was recorded), and result.model_name carries its display name when known. Optional keys: playback_urls (a browser-playable copy per entry of urls, null where the master already plays everywhere — download urls, preview playback_urls), reference_images (reference URLs actually sent to the model), and visual_dna / preset / cinematic_presets, added only when those inputs shaped the generation. 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
400—Neither both frame URLs nor exactly two files were provided, or URLs and files were mixed
400INVALID_DRAFT_MODE / UNSUPPORTED_DRAFT_MODEdraft is not a boolean, or draft: true on a model with no draft tier
400INVALID_PROMPTprompt sent as a non-string
400INVALID_MODELmodel sent as an array or object instead of a string, or the identifier names a hidden (retired) model
400MODEL_NOT_SUPPORTED_ON_THIS_ENDPOINTThe model runs only in Elements or Video to Video
400INVALID_IMAGE_URLA frame URL is empty, not http(s), or points at a private / internal host
400IMAGE_URL_ERROR / FILE_PROCESSING_FAILEDA frame URL could not be downloaded, or a frame could not be processed as an image
400UNSUPPORTED_KLING_RESOLUTIONresolution is not one of the tiers the chosen Kling model family publishes
400UNSUPPORTED_VIDEO_RESOLUTIONA named draft tier such as "480p-draft" without an explicit model that publishes it
400GENERATION_ERRORA visual_dna_ids entry does not exist or you do not have access to it
400SDK_SESSION_INVALID_IDsession_id is not a valid id
404SDK_SESSION_NOT_FOUNDsession_id does not exist or is not a video session
403INSUFFICIENT_CREDITSBalance too low. Checked twice before the generation starts: a minimum-balance gate (10 credits; the response adds balance, required, pricing_url), then a reservation of the model's actual cost
403STORAGE_LIMIT_EXCEEDEDYour storage is full
429—More than 30 accepted requests in the shared standard-limit bucket 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.