Video Editing and Trim
Apply targeted AI edits to an existing video, and cut clips with the frame-accurate trim job.
Two different things live on this page:
POST /api/v1/edit/video— targeted AI edits of an existing video (upscale, reframe, extend, lipsync, inpaint, and more). Async like every other generation: you get ageneration_idand poll/v1/generate/{id}/status.POST /api/v1/video/trim— a plain server-side cut by start/end time. Not an AI generation: it runs as its own job with its own polling endpoint.
POST /api/v1/edit/videoaccepts optionalproject_idandsession_idfields that decide where the result is filed — see Where the edit is filed and Projects.POST /api/v1/video/trimalso accepts aproject_idfield, but the trim job never reads it: it does no project routing, and the trimmed file lands wherever the source lived.
Edit an Existing Video
Endpoint
POST /api/v1/edit/videoRate limited to 30 requests per minute per account. The bucket is shared with the other SDK generation write routes (video, elements, chat, trim, …), and rejected requests do not count against it. Returns 403 with code INSUFFICIENT_CREDITS (plus balance, required and pricing_url) before any work starts when the account cannot cover the edit.
Choosing a model
Every operation runs on its own pool of models. Pass a concrete identifier from GET /api/v1/models?type=<type> as model, or omit model to use the first model of that pool.
operation | Model type for GET /api/v1/models?type= |
|---|---|
upscale | video_upscale |
reframe | video_reframe |
generate_audio | video_to_sound |
remove_watermark | video_watermark_removal |
face_swap | video_face_swap |
extend | video_extend |
magic_edit | video_to_video |
lipsync | lipsync-video |
remove_background | video_background_removal |
inpaint | video_inpaint |
retake | video_retake |
A model that is not in the operation's pool is rejected before any credit is held: 400 with code INVALID_MODEL_FOR_EDIT_TYPE. Identifiers starting with kolbo_gateway_ are web-app navigation aliases, not engines; they are treated as if model were omitted. Model-specific limits, such as the longest source video (max_video_duration), supported resolutions and the extension lengths (min_output_duration/max_output_duration/supported_durations), are listed on each model in that response.
Request Body
video_url and operation are always required. Every other field is read only by the operations listed in its description, and a field sent for the wrong operation is ignored. No field on this route takes an array of reference media: every media input (video_url, image_url, audio_url, mask_video_url) is a single URL.
| Field | Type | Required | Description |
|---|---|---|---|
video_url | string | Yes | URL of the source video. Missing or blank → 400 video_url must be a non-empty string. A URL that points at an internal or private network address → 400 with code INVALID_VIDEO_URL. |
operation | string | Yes | One of upscale, reframe, generate_audio, remove_watermark, face_swap, extend, magic_edit, lipsync, remove_background, inpaint, retake, plus the two draft operations draft_quote and draft_enhance (see Finalize a draft). Missing → 400 operation is required; anything else → 400 listing the valid values. |
model | string | No | Engine identifier for the operation. See Choosing a model. Must be a string, or the request fails with 400 and code INVALID_MODEL. |
prompt | string | No | magic_edit (required). Also read by generate_audio, extend, inpaint, retake and, on two upscale engines, upscale (see mode and enhancement_model). Missing on magic_edit → 400 prompt is required for magic_edit. Must be a string, or the request fails with 400 and code INVALID_PROMPT. Prompt text is screened before any credit is spent. |
scale | number | No | upscale. The upscale factor. Topaz (topaz/upscale/video): 1, 2, 3 or 4, clamped down so the output never exceeds 4K. Omit it to use the largest factor that stays within 4K. 1 means enhance without resizing, which needs an enhancement_model or a target_fps; otherwise the job fails. Flux Video Upscale (blackforestlabs/flux-video-upscale): 1.5–3 (out-of-range values are clamped), default 2. Not used by the Bytedance upscaler, which takes resolution or scale_ratio instead. |
resolution | string | No | upscale (Bytedance only), reframe, draft_quote, draft_enhance. On the Bytedance upscaler: 1080p (default), 2k, 4k, 6k or 8k. 1440p and 2160p are accepted as aliases for 2k/4k; any other value → 400. Other upscale engines ignore it. On reframe only Luma Ray 3.2 Reframe and LTX Reframe read it: the value must be one of the model's supported_resolutions, otherwise the engine's default is used (540p on Luma Ray 3.2, 720p on LTX). Other reframe engines ignore it. |
target_fps | number | No | upscale. Output frame rate. Topaz: enables frame interpolation; it must be between 16 and 60, or the job fails with Invalid target_fps. Bytedance: 24–120, default 30 (not the source rate), and every started 30 fps multiplies the price (60 = 2×, 120 = 4×). An out-of-range value → 400. |
enhancement_model | string | No | upscale, Topaz only. Enhancement preset that picks the Topaz engine and its price, for example Proteus, Iris, Artemis High Quality, Nyx, Starlight Precise 2.6 or Astra 2. The full list, with each preset's cost multiple, is under params.enhancement_model on the Topaz model in GET /api/v1/models?type=video_upscale. Omit it, or send auto, for the standard upscale. An unknown name falls back to the default engine. With Astra 2, prompt guides the result; prompt-guided Astra jobs accept at most 450 source frames. |
mode | string | No | upscale (Flux Video Upscale): "precise" (default, source-faithful) or "creative" (detail enhancement, priced higher); any other value means precise. On this engine, prompt guides creative-mode detail. extend: "start" or "end", default "end". retake: "replace_audio", "replace_video" or "replace_audio_and_video"; any other value falls back to "replace_audio_and_video". |
enhancement_tier | string | No | upscale, Bytedance (bytedance-upscaler/upscale/video). fast (half price), standard (default) or pro (large-model restoration, 10× price). |
enhancement_preset | string | No | upscale, Bytedance. Content type: general (default), ugc, short_series, aigc, old_film. Same price for all. |
fidelity | string | No | upscale, Bytedance. high (default, mild and source-faithful) or medium (stronger enhancement). |
bit_depth | number | No | upscale, Bytedance. 8 (default), 10 or 12. 10 and 12 need enhancement_tier: "pro" (otherwise 400) and are delivered as HEVC. |
scale_ratio | number | No | upscale, Bytedance. Custom scale 1.1–10, used instead of resolution. The scaled output must stay within 4K (long edge ≤ 3840, short edge ≤ 2160), otherwise 400. Use resolution: "6k" or "8k" to go above 4K. |
aspect_ratio | string | No | reframe (required). Target aspect ratio, e.g. "16:9", "9:16", "1:1". Check the model's supported_aspect_ratios: LTX Reframe maps an unsupported ratio to the nearest one it supports (21:9→16:9, 4:3→5:4, 3:4→4:5, 9:21→9:16). Missing → 400 aspect_ratio is required for reframe. |
grid_position_x | number | No | reframe. Horizontal placement (0.0–1.0) of the original content inside the reframed canvas. Omit it to let the model centre the content. A value of exactly 0 is treated as unset. Ignored by luma-ray-3-2-reframe and ltx-2.3/reframe. |
grid_position_y | number | No | reframe. Vertical placement (0.0–1.0), with the same rules as grid_position_x. |
sound_effect_prompt | string | No | generate_audio, Kling Video to Audio. Prompt for the sound-effects layer. On Kling, a missing value falls back to prompt. |
background_music_prompt | string | No | generate_audio, Kling Video to Audio. Prompt for the background-music layer. Kling always sends one, so omitting it makes the server use its own placeholder. Send it explicitly. |
original_sound | boolean | No | generate_audio, Pixverse Sound. Keep the original audio and mix the generated audio in on top. |
cfg_strength | number | No | generate_audio, MMAudio. Guidance strength, default 4.5. Higher values follow the prompt more strictly. |
audio_format | string | No | generate_audio, Sonilo models only (identifier contains sonilo). Validated against that model's output_audio_formats from GET /api/v1/models; when that list is empty the check falls back to wav, mp3, aac, flac. An unlisted value fails the job with Invalid audio_format "…". Omit it to use the model's default_output_audio_format, falling back to aac. Only Sonilo Sound Effects sends the value to the provider; Sonilo Score validates it and then ignores it. |
segments | array | No | generate_audio, Sonilo Sound Effects only. Per-segment audio instructions, sent to the provider unchanged. Dropped unless it is a non-empty array. |
image_url | string | No | face_swap (required). URL of the reference face image. Missing → 400 image_url (reference face) is required for face_swap. |
duration | number | No | extend, retake. Seconds of content to generate. On extend the default is 5, clamped into the chosen model's own extension range (falling back to 1–20 when the model declares none). On retake the default and the minimum are 2 seconds, and the segment is clamped so start_time + duration stays inside the source. |
context | number | No | extend. How much of the source the LTX extend engine uses as context. The official LTX path drops non-numeric values, and FLUX 3 extend never sends this field. |
audio_url | string | No | lipsync. URL of the audio track to sync to. Required unless text_prompt is given. |
text_prompt | string | No | lipsync. Text to synthesize and sync to, as an alternative to audio_url. With neither → 400 audio_url or text_prompt is required for lipsync. |
refine_edges | boolean | No | remove_background, Veed engine only. Edge refinement for a cleaner cutout. Default true. Other background-removal engines ignore it. |
subject_is_person | boolean | No | remove_background, Veed engine only. Optimize for a human subject. Default true; set it to false when the subject is not a person. Other background-removal engines ignore it. |
mask_video_url | string | inpaint | inpaint (required in practice). Black-and-white mask video; white marks the area to fill. It must be an https URL on Kolbo storage (*.kolbo.ai, *.digitaloceanspaces.com or *.amazonaws.com), so upload it through the Media Library first. The API has no way to draw a mask for you, so without one the job fails with Inpaint requires a mask …, and a mask hosted anywhere else fails with Invalid mask …. |
object_prompt | string | No | inpaint. Description of the object being replaced. |
video_strength | number | No | inpaint. How closely to follow the original video, 0.0–1.0 (higher stays closer to the source). |
start_time | number | No | retake. Start of the segment to regenerate, in seconds. Default 0. Negative values are raised to 0, and a value later than source_duration − 2s is pulled back so a full 2-second segment still fits. |
project_id | string | No | Project to file the edit into. See Where the edit is filed. |
session_id | string | No | An existing video session to file the edit into. It wins over project_id. See Where the edit is filed. |
generate_audio needs some audio instruction: prompt, or sound_effect_prompt, or background_music_prompt. Models whose identifier contains sonilo caption the video themselves, so they accept the request with none of these. Otherwise you get 400. Which prompt field a model reads depends on the engine: Kling reads sound_effect_prompt/background_music_prompt, while MMAudio, Thinksound, Pixverse and Sonilo read prompt.
remove_watermark and face_swap (apart from image_url) take no operation-specific fields. inpaint also needs prompt unless the selected model does not require one (the Bria eraser engine takes none). Without it the job fails with This inpaint model needs a prompt describing the result. If GET /api/v1/models?type=video_inpaint returns no models, inpaint is not currently available.
Long sources. upscale and reframe check the source against the selected model's max_video_duration at submit time and reject a longer clip with 400 and code VIDEO_TOO_LONG before charging. The Bytedance upscaler tightens its limit further for 6k, 8k and the pro tier. extend and retake cap the source at 30 seconds; anything longer fails the job with a Source video is too long … error. Some extend models are stricter: FLUX 3 extend needs a source under 15 seconds, and the Seedance extend models enforce their own minimum and maximum source length. retake additionally needs at least 3 seconds of source.
Billing. retake bills by the length of the whole input video, not the segment you asked for, so trim a long file with POST /api/v1/video/trim before retaking it. LTX and Grok extend bill the generated extension plus about a second of context frames, capped at 21 billed seconds. FLUX 3 extend bills only the generated seconds. Seedance extend bills the source seconds plus the extension. resolution is not forwarded for extend, so through this route Seedance and FLUX 3 extend always render at 720p.
Where the edit is filed
- With
session_id, the edit lands in that session. The session can be any of your text-to-video, image-to-video, video-to-video, lipsync or video-edit sessions, in a project you can edit.project_idis then ignored. A malformed id returns400 SDK_SESSION_INVALID_ID, and an unknown or deleted session returns404 SDK_SESSION_NOT_FOUND. A session in a project you cannot see returns404 SDK_PROJECT_NOT_FOUND, and one in a project where you only have view access returns403 SDK_PROJECT_ACCESS_DENIED. - Without
session_id, whenvideo_urlis exactly the URL of a video you generated on Kolbo (as listed in your media library), the edit joins that video's session. Aproject_idnaming a different project overrides this. - Otherwise, each edit starts a new
API Video Edit - <date>video-to-video session inproject_id, or in your auto-created API Generations project when you omit it. A malformedproject_idreturns400 SDK_PROJECT_INVALID_ID, an unknown or inaccessible one404 SDK_PROJECT_NOT_FOUND, and a view-only one403 SDK_PROJECT_ACCESS_DENIED.
Examples
Reframe a landscape clip to vertical:
curl -X POST https://api.kolbo.ai/api/v1/edit/video \
-H "X-API-Key: kolbo_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://cdn.kolbo.ai/videos/source.mp4",
"operation": "reframe",
"model": "wan-vace-14b/reframe",
"aspect_ratio": "9:16",
"grid_position_x": 0.5,
"grid_position_y": 0.4
}'Add a sound-effects and music bed:
curl -X POST https://api.kolbo.ai/api/v1/edit/video \
-H "X-API-Key: kolbo_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://cdn.kolbo.ai/videos/source.mp4",
"operation": "generate_audio",
"model": "kling-video/video-to-audio",
"sound_effect_prompt": "Footsteps on gravel, distant traffic",
"background_music_prompt": "Sparse ambient pads, slow tempo"
}'Lipsync to an audio track:
curl -X POST https://api.kolbo.ai/api/v1/edit/video \
-H "X-API-Key: kolbo_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://cdn.kolbo.ai/videos/speaker.mp4",
"operation": "lipsync",
"audio_url": "https://cdn.kolbo.ai/audio/voiceover.mp3"
}'Poll for the result (JavaScript):
const API_KEY = "kolbo_live_YOUR_API_KEY";
const start = await fetch("https://api.kolbo.ai/api/v1/edit/video", {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
video_url: "https://cdn.kolbo.ai/videos/source.mp4",
operation: "upscale",
model: "bytedance-upscaler/upscale/video",
resolution: "4k",
target_fps: 60
})
}).then((r) => r.json());
while (true) {
await new Promise((r) => setTimeout(r, start.poll_interval_hint * 1000));
const status = await fetch(
`https://api.kolbo.ai/api/v1/generate/${start.generation_id}/status`,
{ headers: { "X-API-Key": API_KEY } }
).then((r) => r.json());
if (status.state === "completed") {
console.log("Edited video:", status.result.urls[0]);
break;
}
if (status.state === "failed") {
console.error(status.error);
break;
}
}Response
Edit started:
{
"success": true,
"generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
"type": "global_video_edit",
"model": "your-model-identifier",
"credits_charged": 40,
"poll_url": "/v1/generate/68f2c1a9b4e5d6f7a8b9c0d1/status",
"poll_interval_hint": 8,
"session_id": "…",
"project_id": "…"
}credits_charged is null when the pipeline has not settled the cost at submit time. The authoritative figure is credits_used on the completed status. model is the concrete engine the server resolved for the edit. When you omit model, it names the default it picked, not null. session_id and project_id say where the edit was filed.
Submit errors use the standard SDK shape { "success": false, "error": "…", "code": "…" } and keep the HTTP status. Besides the field errors in the table above, you may see 400 INVALID_MODEL_FOR_EDIT_TYPE, 400 VIDEO_TOO_LONG, 400 VIDEO_DURATION_UNAVAILABLE (duration-capped models could not read the source length), 400 INVALID_UPSCALE_OPTIONS (a bad Bytedance option combination), 400 UNSUPPORTED_RESOLUTION / INVALID_DURATION / VIDEO_TOO_SHORT (Seedance extend), and 403 INSUFFICIENT_CREDITS or 403 INSUFFICIENT_PERMISSION (no edit permission on the target project). Problems found while the job runs, such as an out-of-range target_fps on Topaz or an over-length extend source, are reported as a failed state on the status endpoint instead.
Completed status:
{
"success": true,
"generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
"type": "global_video_edit",
"state": "completed",
"progress": 100,
"credits_used": 40,
"result": {
"urls": ["https://cdn.kolbo.ai/videos/..."],
"download_url": "https://cdn.kolbo.ai/videos/...",
"edit_type": "reframe",
"duration": 12,
"model": "the-model-that-actually-ran",
"created_at": "2026-04-12T10:00:00.000Z"
}
}edit_type is the internal name of the operation, which is not always identical to the operation you sent — for example generate_audio reports audio_generation, remove_watermark reports watermark_removal, face_swap reports video_face_swap, and remove_background reports background_removal. result.model is the engine that actually ran.
Finalize a draft
Models that publish draft capabilities in GET /api/v1/models, such as Seedance 2.5 at resolution: "480p-draft", produce a cheap preview that you can later render at full quality. Both draft operations take only video_url (the saved draft, which must be your own generation) and an optional resolution. The server picks the finalization engine itself, so model and prompt are not used.
draft_quote checks the draft and prices the render without starting it or charging anything. It skips the credit gate, does not go through the generation tracker, and returns the quote directly:
{
"status": true,
"data": {
"modelId": "…",
"modelName": "…",
"sourceResolution": "480p",
"resolution": "1080p",
"supportedResolutions": ["720p", "1080p"],
"duration": 5,
"inputSeconds": 0,
"hasVideoInput": false,
"credits": 120,
"expiresAt": "2026-10-06T10:00:00.000Z",
"previousRenders": []
}
}resolution defaults to the first of supportedResolutions. expiresAt is null for drafts that do not expire. previousRenders lists earlier full-quality renders of this draft (generationId, resolution, status, and videoUrl once completed), so you can reuse one instead of paying twice. previousCredits appears only while a price reduction is running. Quote errors use the in-app shape { "status": false, "code": "…", "message": "…" }, with these codes: DRAFT_NOT_FOUND (404), DRAFT_EXPIRED (410), INVALID_DRAFT (400, for example a model that does not support draft rendering), DRAFT_RESOLUTION_UNSUPPORTED, DRAFT_DURATION_UNAVAILABLE and DRAFT_INPUT_DURATION_UNAVAILABLE (400), DRAFT_ENGINE_UNAVAILABLE and DRAFT_PRICING_UNAVAILABLE (503), and DRAFT_QUOTE_FAILED (500) for anything else.
draft_enhance starts the paid render. It returns the normal edit-started response and is polled like any other edit. Each draft renders once per resolution: a second request returns 409 with code DRAFT_RENDER_IN_PROGRESS while the first is still running, or DRAFT_ALREADY_RENDERED afterwards. To make retries safe, send an Idempotency-Key header of 8–128 characters (A-Z, a-z, 0-9, _ . : -); any other value returns 400 INVALID_IDEMPOTENCY_KEY. Replaying the same key for the same draft and resolution returns the original generation_id instead of starting a second render; reusing it for a different draft or resolution returns 409 IDEMPOTENCY_KEY_CONFLICT.
Trim a Video
A frame-accurate server-side cut. This is not an AI generation: it does not go through the generation tracker, it is not pollable via /v1/generate/{id}/status, and no SDK credit gate is applied to it.
Endpoint
POST /api/v1/video/trim
GET /api/v1/video/trim/{jobId}The POST is rate limited to 30 requests per minute per account, in the same bucket as the other SDK generation write routes. The status GET is limited to 120 per minute.
Completion contract
Polling is the only completion mechanism. No webhook, no callback_url field on the trim request body, no server-sent events. Submit with POST /v1/video/trim, then loop against GET /api/v1/video/trim/{jobId} until status is completed or failed.
Read Polling & Cancellation for the shared concepts — poll intervals, transient-error tolerance, back-off. A trim diverges from that generic contract in almost every field name:
| Generic generation contract | Video Trim |
|---|---|
Poll GET /v1/generate/{id}/status | Poll GET /v1/video/trim/{jobId}. A trim job lives in its own store and is never registered in the SDK generation tracker. Its id is not an ObjectId either, so the generic route rejects it outright with 400 Invalid generation ID format. |
Submit returns 200 | Submit returns 202 Accepted. Do not test for 200 alone. |
| Ids are Mongo ObjectIds | The id is a prefixed string like video-trim_1753612345678_k3f9d2p1a. Pass it verbatim. |
poll_url (no /api prefix) | pollingUrl, and it points at the in-app route (/api/video-trim/{jobId}/progress), not the public API. Ignore it — see the callout below. |
state: pending, processing, completed, failed, cancelled | status, lowercased server-side, with a wider vocabulary: initializing, downloading, analyzing, processing, uploading, completed, failed. Not every value appears on every job — downloading only shows up when the clip is processed locally rather than offloaded. Treat anything that is not completed or failed as still running rather than enumerating them. |
result.urls[] | Four sibling top-level fields — url, videoUrl, fileUrl, publicUrl — all carrying the same string. Read url. |
credits_used | Not returned. A trim is a utility operation, not a billed generation. |
error string plus error_code | A single error string, null while the job is healthy. |
POST /v1/generate/{id}/cancel | No public cancel route. cancelled exists in the status vocabulary but nothing in the public API can put a job into it. |
| Timestamps are ISO 8601 strings | updatedAt is a Unix timestamp in milliseconds. |
Submit returns poll_interval_hint | Not returned. Nothing in the trim submit response suggests an interval — choose your own. Trims usually finish in seconds, so 2–4 s is reasonable; the examples on this page use 4. |
| The server force-fails a generation past its own hard deadline | No server deadline is published for trim jobs, and there is no cancel route. Set your own client-side give-up window and treat expiry as a display decision only: the job keeps running, and a trim is not billed as a generation so nothing is at stake if you stop watching. |
Because a trim is not registered as an SDK generation, the WebSocket suppression that silences every API-key generation does not apply to it: the trim service still emits its in-app generation:progress events (type: "video-trim") into the owner's user room. Those events are an internal implementation detail of the web app, not part of the public API contract — they are not documented, not versioned, and can be removed. Poll.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
video_url | string | Yes | URL of the source video. Missing → 400 video_url is required. The URL is validated against internal and private network addresses; one that points at such an address is rejected at submit with 500 { "success": false, "error": "…" }. |
start_time | number | Yes | Trim start in seconds. Must be a JSON number, not a string. A value at or beyond the source video's duration fails the job. |
end_time | number | Yes | Trim end in seconds. Must be a JSON number. A value beyond the source video's duration is clamped down to the real duration rather than failing. |
project_id | string | No | Accepted by the request but not used — the trim job does no project routing. |
Sending start_time or end_time as anything other than a number returns 400 start_time and end_time (seconds, numbers) are required.
The original video is never deleted by this endpoint — the trim always produces a new file.
Submit Response (202)
{
"success": true,
"jobId": "video-trim_1753612345678_k3f9d2p1a",
"generationId": "video-trim_1753612345678_k3f9d2p1a",
"status": "processing",
"message": "Video trim job started",
"pollingUrl": "/api/video-trim/video-trim_1753612345678_k3f9d2p1a/progress"
}Ignore pollingUrl. It points at the in-app route, not the public API. Poll GET /api/v1/video/trim/{jobId} instead, using the jobId from this response. generationId is the same value under a legacy field name.
Status Response
{
"success": true,
"jobId": "video-trim_1753612345678_k3f9d2p1a",
"generationId": "video-trim_1753612345678_k3f9d2p1a",
"status": "completed",
"progress": 100,
"message": "…",
"error": null,
"updatedAt": 1753612389421,
"url": "https://cdn.kolbo.ai/videos/trimmed.mp4",
"videoUrl": "https://cdn.kolbo.ai/videos/trimmed.mp4",
"fileUrl": "https://cdn.kolbo.ai/videos/trimmed.mp4",
"publicUrl": "https://cdn.kolbo.ai/videos/trimmed.mp4"
}status is normalized to lowercase. The four URL fields carry the same value and appear only once the trimmed file exists — read url. progress climbs 0→100 but is not monotonic across the two execution paths (a heavy clip offloaded to the hosted trimmer reports a different curve than a locally processed one), so drive your logic off status, not progress.
| Field | Type | When | Description |
|---|---|---|---|
success | boolean | always | true on any readable job — it is not the job state |
jobId | string | always | The id you polled with |
generationId | string | always | The same value under a legacy field name |
status | string | always | Lowercased phase — see the vocabulary above |
progress | number | always | 0-100 |
message | string | always | Human-readable step, e.g. "Trimming video... 42%" |
error | string | null | always | null unless status is failed |
updatedAt | number | always | Unix milliseconds |
url / videoUrl / fileUrl / publicUrl | string | once the file exists | Identical values. Read url. |
Failure shape
A failed trim is a successful poll — HTTP 200, success: true, with the failure in the payload:
{
"success": true,
"jobId": "video-trim_1753612345678_k3f9d2p1a",
"generationId": "video-trim_1753612345678_k3f9d2p1a",
"status": "failed",
"progress": 20,
"message": "Trimming video...",
"error": "ffmpeg exited with code 1: …",
"updatedAt": 1753612389421
}progress freezes wherever the job died and the URL fields are absent. The error string is the raw failure reason — there is no error code on this route.
Transport-level errors are different: an unknown or expired jobId returns 404 { "success": false, "error": "Trim job not found" }, and a job belonging to another account returns 403 { "success": false, "error": "Access denied to this trim job" }.
There is no public endpoint to cancel a trim job. Once submitted, a trim runs to completed or failed.
Example
# Submit
curl -X POST https://api.kolbo.ai/api/v1/video/trim \
-H "X-API-Key: kolbo_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://cdn.kolbo.ai/videos/source.mp4",
"start_time": 12.5,
"end_time": 41
}'
# Poll (jobId from the submit response)
curl https://api.kolbo.ai/api/v1/video/trim/video-trim_1753612345678_k3f9d2p1a \
-H "X-API-Key: kolbo_live_YOUR_API_KEY"import requests, time
API_KEY = "kolbo_live_YOUR_API_KEY"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
job = requests.post(
"https://api.kolbo.ai/api/v1/video/trim",
headers=HEADERS,
json={
"video_url": "https://cdn.kolbo.ai/videos/source.mp4",
"start_time": 12.5,
"end_time": 41,
},
).json()
job_id = job["jobId"]
while True:
time.sleep(4)
status = requests.get(
f"https://api.kolbo.ai/api/v1/video/trim/{job_id}",
headers={"X-API-Key": API_KEY},
).json()
if status["status"] == "completed":
print("Trimmed video:", status["url"])
break
if status["status"] == "failed":
print("Error:", status.get("error"))
breakTrims usually finish in seconds. Longer or heavier clips are offloaded to a hosted trimming service instead of being processed locally, which is transparent to the caller but can add time.
Related
Polling & Cancellation
The shared async contract — and why polling is the only completion mechanism
Video Generation
Text-to-video and image-to-video
Video-to-Video
Restyle, motion-transfer, or subtitle an existing video
Lipsync
The dedicated lipsync generation endpoint
Media Library
Upload source videos and retrieve the resulting files