Claude Opus 5 is now live on CometAPI →

Hoe AI-videogeneratie aan een SaaS-app toevoegen

CometAPI
AnnaJun 5, 2026
Hoe AI-videogeneratie aan een SaaS-app toevoegen

Videogeneratie aan je app toevoegen is niet hetzelfde als afbeeldingsgeneratie. De API-call keert direct terug — maar de video is dan nog niet klaar. Je krijgt een taak-id, en je moet blijven vragen “is het klaar?” tot het zover is.

De meeste ontwikkelaars lopen hier tegenaan wanneer ze voor het eerst een video-API aanroepen, wachten op een response body met een video-URL, en in plaats daarvan een taak-id terugkrijgen. Deze gids loopt de volledige flow door: een taak indienen, voor resultaten poll-en, fouten afhandelen en de output opslaan voordat de URL verloopt.

Wat je gaat bouwen

Een back-endservice die een tekstprompt of afbeelding accepteert, een videogeneratietaak indient, blijft poll-en tot de taak klaar is, en de definitieve video-URL teruggeeft. Je werkt met vier modellen — Veo 3 Fast, Sora 2, Kling Video en Runway — allemaal via één API-sleutel.

Vereisten:

  • Python 3.8+ of Node.js 18+
  • Een CometAPI-sleutel
  • Basiskennis van REST-API's

Begrijp waarom videogeneratie anders is

Bij afbeeldingsgeneratie stuur je een request en krijg je de afbeelding terug in dezelfde response. Videogeneratie gebruikt een asynchrone taakqueue:

  1. Indienen van een generatierequest → je krijgt een task_id terug
  2. Poll elke paar seconden een status-endpoint
  3. Wanneer de status een terminale staat bereikt, krijg je de video-URL
  4. Download en sla op — de URL is tijdelijk

Als je videogeneratie behandelt zoals afbeeldingsgeneratie en wacht tot de eerste response je video bevat, zal je request elke keer time-outen.

In een productie-webservice hoort deze polling-loop in een background worker te draaien (Celery, Bull of vergelijkbaar), niet in je request handler. De voorbeelden hieronder gebruiken synchrone polling — prima voor scripts en prototypes, maar niet voor het afhandelen van gelijktijdige gebruikers.

Kies een model

ModelAanbiederMaximale duurPrijs (via CometAPI)Het best voor
Veo 3 FastGoogle8 sec$0.05/secSnel prototypen, sociale clips
Sora 2OpenAI (via CometAPI-model-ID)~10 sec$0.08/secKorte creatieve video's van hoge kwaliteit
Kling VideoKuaishou10 sec$0.13–$2.64/taskMarketingcontent, fijnmazige controle
Runway Gen-3A TurboRunway5 of 10 sec$0.32/taskImage-to-video, commerciële content

Bron**: CometAPI-modelpagina's, mei 2026. Opmerking: “Sora 2” is CometAPI's model identificator — raadpleeg hun modelpagina voor details over het onderliggende model.

  • Veo 3 Fast ondersteunt zowel text-to-video als image-to-video. De goedkoopste per seconde, goed startpunt.
  • Sora 2 genereert audio native samen met de video — dialoog, omgevingsgeluid en effecten zonder aparte TTS-stap.
  • Kling Video biedt negative_prompt, cfg_scale, instellingen voor camerabeweging en een pro-modus. Meeste controle van de vier.
  • Runway is via CometAPI alleen image-to-video. Geef een statische afbeelding en een bewegingsbeschrijving, en het animeert die.

Een Veo-taak indienen

Veo gebruikt multipart/form-data. Gebruik files= in Python requests om dit correct te versturen — data=dict stuurt application/x-www-form-urlencoded, wat niet hetzelfde is:

import requestsimport osfrom dotenv import load_dotenv​load_dotenv()​def submit_veo_task(prompt: str, size: str = "16x9") -> str:    """Submit a Veo 3 Fast text-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    response = requests.post(        "https://api.cometapi.com/v1/videos",        headers={"Authorization": f"Bearer {api_key}"},        files={            "prompt": (None, prompt),            "model": (None, "veo3-fast"),            "size": (None, size)        },        timeout=30    )    response.raise_for_status()    return response.json()["id"]​​task_id = submit_veo_task("A paper kite drifting above a wheat field on a windy afternoon")print(f"Task submitted: {task_id}")

Resultaat opvragen (polling)

import time​def poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Poll until Veo task completes. Returns video URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    headers = {"Authorization": f"Bearer {api_key}"}    url = f"https://api.cometapi.com/v1/videos/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result.get("status")​        if status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(                f"Task {task_id} failed with status '{status}': "                f"{result.get('error', 'no error detail returned')}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Task {task_id} did not complete within {max_wait} seconds")​​video_url = poll_veo_task(task_id)print(f"Video ready: {video_url}")

Kling Video gebruiken voor meer controle

Kling heeft een andere endpoint-structuur en gebruikt JSON. Let op dat Kling’s terminale statusstring "succeed" is (niet "succeeded") — dit komt overeen met het daadwerkelijke response-formaat van de API:

def submit_kling_task(prompt: str, duration: str = "5", mode: str = "std") -> str:    """Submit a Kling text-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    response = requests.post(        "https://api.cometapi.com/kling/v1/videos/text2video",        headers={            "Authorization": f"Bearer {api_key}",            "Content-Type": "application/json"        },        json={            "model_name": "kling-v1-6",            "prompt": prompt,            "negative_prompt": "blurry, low quality, watermark",            "cfg_scale": 0.5,            "mode": mode,         # "std" or "pro"            "aspect_ratio": "16:9",            "duration": duration  # "5" or "10"        },        timeout=30    )    response.raise_for_status()    return response.json()["data"]["task_id"]​​def poll_kling_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Poll Kling task until complete. Returns video URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    headers = {"Authorization": f"Bearer {api_key}"}    url = f"https://api.cometapi.com/kling/v1/videos/text2video/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result["data"]["task_status"]​        if status == "succeed":  # Kling uses "succeed", not "succeeded"            return result["data"]["task_result"]["videos"][0]["url"]        elif status == "failed":            error_detail = result.get("data", {}).get("task_result", "no detail")            raise RuntimeError(                f"Kling task {task_id} failed: {error_detail}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Kling task {task_id} timed out after {max_wait}s")

Bron**: CometAPI Kling Video docs

Een statische afbeelding animeren met Runway

Runway is alleen image-to-video. Het vereist ook een extra header (X-Runway-Version):

def submit_runway_task(image_url: str, motion_prompt: str, duration: int = 5) -> str:    """Submit a Runway image-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    response = requests.post(        "https://api.cometapi.com/runwayml/v1/image_to_video",        headers={            "Authorization": f"Bearer {api_key}",            "X-Runway-Version": "2024-11-06",            "Content-Type": "application/json"        },        json={            "model": "gen3a_turbo",            "promptImage": image_url,  # must be a stable HTTPS URL            "promptText": motion_prompt,            "duration": duration,            "ratio": "1280:720",            "watermark": False        },        timeout=30    )    response.raise_for_status()    return response.json()["id"]​​def poll_runway_task(task_id: str, interval: int = 5, max_wait: int = 600) -> str:    """Poll Runway task. Returns video URL when done."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    headers = {        "Authorization": f"Bearer {api_key}",        "X-Runway-Version": "2024-11-06"    }    url = f"https://api.cometapi.com/runwayml/v1/tasks/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result.get("status")​        if status == "task_not_exist":            # CometAPI-specific: task is still initializing, retry after a few seconds            time.sleep(interval)            elapsed += interval            continue        elif status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(f"Runway task {task_id} failed: {result.get('error', 'no detail')}")​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Runway task {task_id} timed out after {max_wait}s")

Bron**: CometAPI Runway docs

Sla de video op voordat de URL verloopt

Video-URL’s van generatieve API’s zijn tijdelijk. Download het bestand meteen en sla het op op een plek die je beheert:

import requestsimport pathlib​def download_video(url: str, output_path: str) -> None:    """Download video from URL to local file using streaming."""    out = pathlib.Path(output_path)    if out.parent != pathlib.Path("."):        out.parent.mkdir(parents=True, exist_ok=True)​    with requests.get(url, stream=True, timeout=60) as r:        r.raise_for_status()        with open(out, "wb") as f:            for chunk in r.iter_content(chunk_size=8192):                f.write(chunk)    print(f"Saved to {output_path}")​​# Full flowtask_id = submit_veo_task("A timelapse of clouds moving over a city skyline")video_url = poll_veo_task(task_id)download_video(video_url, "output/city_timelapse.mp4")

In productie vervang je het lokaal wegschrijven door een upload naar S3, Cloudflare R2 of opslag naar keuze. Het streamingpatroon blijft hetzelfde — stuur de bytes direct door in plaats van de hele video in het geheugen te laden.

Fouten afhandelen

SymptoomWaarschijnlijke oorzaakOplossing
Taak blijft 10+ min in de wachtrij stekenServerbelasting of model niet beschikbaarProbeer opnieuw met een ander model
task_not_exist bij de eerste Runway-pollTaak wordt nog geïnitialiseerdWacht 5 sec en probeer opnieuw — gedocumenteerd CometAPI-gedrag
mislukt zonder foutmeldingPrompt activeerde contentfilterFormuleer de prompt anders
Video-URL geeft 403 terugURL verlopen vóór downloadDownload direct nadat je de URL ontvangt
Time-out na 10 minGeneratie duurde te langVerhoog max_wait of schakel over naar Veo 3 Fast
Kling retourneert "succeed" en niet "succeeded"De API van Kling gebruikt een niet-standaard statusstringDit is correct — zie de Kling-pollingcode hierboven

Bron: CometAPI video generation docs

Node.js-versie

Node.js 18+ bevat fetch en FormData native. Dit voorbeeld dekt alle vier de modellen:

/* Node.js 18+ — no extra packages needed​*/const API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY is not set');​// --- Veo 3 Fast ---async function submitVeoTask(prompt, size = '16x9') {  const form = new FormData();  form.append('prompt', prompt);  form.append('model', 'veo3-fast');  form.append('size', size);​  const res = await fetch('https://api.cometapi.com/v1/videos', {    method: 'POST',    headers: { 'Authorization': `Bearer ${API_KEY}` },    body: form  });  if (!res.ok) throw new Error(`Veo submit failed: ${res.status}`);  return (await res.json()).id;}​async function pollVeoTask(taskId, intervalMs = 10000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(`https://api.cometapi.com/v1/videos/${taskId}`, {      headers: { 'Authorization': `Bearer ${API_KEY}` }    });    if (!res.ok) throw new Error(`Poll failed: ${res.status}`);    const result = await res.json();​    if (result.status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(result.status)) {      throw new Error(`Task ${taskId} failed: ${result.error ?? 'no detail'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Task ${taskId} timed out`);}​// --- Kling Video ---async function submitKlingTask(prompt, duration = '5', mode = 'std') {  const res = await fetch('https://api.cometapi.com/kling/v1/videos/text2video', {    method: 'POST',    headers: {      'Authorization': `Bearer ${API_KEY}`,      'Content-Type': 'application/json'    },    body: JSON.stringify({      model_name: 'kling-v1-6',      prompt,      negative_prompt: 'blurry, low quality, watermark',      cfg_scale: 0.5,      mode,      aspect_ratio: '16:9',      duration    })  });  if (!res.ok) throw new Error(`Kling submit failed: ${res.status}`);  return (await res.json()).data.task_id;}​async function pollKlingTask(taskId, intervalMs = 10000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(      `https://api.cometapi.com/kling/v1/videos/text2video/${taskId}`,      { headers: { 'Authorization': `Bearer ${API_KEY}` } }    );    if (!res.ok) throw new Error(`Kling poll failed: ${res.status}`);    const result = await res.json();    const status = result.data.task_status;​    if (status === 'succeed') return result.data.task_result.videos[0].url;    if (status === 'failed') {      throw new Error(`Kling task ${taskId} failed: ${JSON.stringify(result.data.task_result ?? 'no detail')}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Kling task ${taskId} timed out`);}​// --- Runway (image-to-video) ---async function submitRunwayTask(imageUrl, motionPrompt, duration = 5) {  const res = await fetch('https://api.cometapi.com/runwayml/v1/image_to_video', {    method: 'POST',    headers: {      'Authorization': `Bearer ${API_KEY}`,      'X-Runway-Version': '2024-11-06',      'Content-Type': 'application/json'    },    body: JSON.stringify({      model: 'gen3a_turbo',      promptImage: imageUrl,      promptText: motionPrompt,      duration,      ratio: '1280:720',      watermark: false    })  });  if (!res.ok) throw new Error(`Runway submit failed: ${res.status}`);  return (await res.json()).id;}​async function pollRunwayTask(taskId, intervalMs = 5000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(      `https://api.cometapi.com/runwayml/v1/tasks/${taskId}`,      { headers: { 'Authorization': `Bearer ${API_KEY}`, 'X-Runway-Version': '2024-11-06' } }    );    if (!res.ok) throw new Error(`Runway poll failed: ${res.status}`);    const result = await res.json();    const status = result.status;​    if (status === 'task_not_exist') {      // CometAPI-specific: task still initializing      await new Promise(r => setTimeout(r, intervalMs));      elapsed += intervalMs;      continue;    }    if (status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(status)) {      throw new Error(`Runway task ${taskId} failed: ${result.error ?? 'no detail'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Runway task ${taskId} timed out`);}​// Usage exampleconst taskId = await submitVeoTask('A paper kite drifting above a wheat field');const videoUrl = await pollVeoTask(taskId);console.log('Video ready:', videoUrl);

Wat nu

Je hebt nu werkende code voor vier videomodellen, een polling-loop die fouten afhandelt, en een downloadstap zodat je gegenereerde content niet verliest.

Het volgende probleem waar de meeste ontwikkelaars tegenaan lopen: ze hebben één model hardgecodeerd, en overschakelen naar een goedkoper of sneller alternatief vereist aanpassingen in meerdere bestanden. Het volgende artikel behandelt hoe je requests over modellen routeert zonder je code te herschrijven.

Volgende: Hoe wissel je tussen AI-modellen zonder je code te herschrijven

Veelgestelde vragen

Q: Waarom krijg ik een taak-id in plaats van een video in de API-response?

Videogeneratie is asynchroon — modellen zoals Veo, Sora, Kling en Runway hebben 2–5 minuten nodig om te renderen. De API retourneert meteen een taak-id zodat je request geen time-out krijgt. Je pollt een apart status-endpoint tot de taak een terminale staat bereikt (succeeded, succeed, failed).

Q: Hoelang blijft een gegenereerde video-URL geldig?

Video-URL’s van generatieve API’s zijn tijdelijk. Download het bestand direct nadat je de URL hebt en sla het op in je eigen opslag (S3, Cloudflare R2, enz.). Sla de URL niet op met de verwachting dat die uren later nog werkt.

Q: Wat is het verschil tussen Veo 3 Fast en Kling Video?

Veo 3 Fast is goedkoper ($0.05/sec), sneller en eenvoudiger aan te roepen. Kling Video geeft je meer controle: negative_prompt, cfg_scale, instellingen voor camerabeweging en een pro-kwaliteitsmodus. Heb je fijne afstemming nodig, gebruik Kling. Heb je snelheid en lage kosten nodig, gebruik Veo 3 Fast.

Q: Kan ik video genereren vanuit een afbeelding in plaats van een tekstprompt?

Ja. Veo ondersteunt image-to-video door een input_reference-bestand mee te geven. Kling ondersteunt dit via het /kling/v1/videos/image2video-endpoint met een image-parameter (URL of base64). Runway is uitsluitend image-to-video — het accepteert geen tekst-only prompts via CometAPI.

Q: Waarom geeft Runway task_not_exist bij de eerste poll terug?

Dit is gedocumenteerd CometAPI-gedrag — de taak wordt nog geïnitialiseerd op de backend. Wacht een paar seconden en probeer opnieuw. Het is geen fout. De pollingcode hierboven handelt dit automatisch af.

Q: Waarom gebruikt Kling "succeed" in plaats van "succeeded"?

Dat is het daadwerkelijke response-formaat van Kling’s API. Het is geen typefout. Veo en Runway gebruiken "succeeded" — Kling gebruikt "succeed". Als je een uniforme polling-wrapper bouwt, moet je beide strings afhandelen.

Q: Is de synchrone polling-loop veilig om in een webserver te gebruiken?

Nee. De polling-loop in deze gids blokkeert de thread minutenlang. In een echte webservice met gelijktijdige gebruikers laat je de polling in een background worker draaien (Celery voor Python, Bull voor Node.js). Dien de taak in de request handler in, retourneer de taak-id naar de client, en laat de worker de client informeren wanneer de video klaar is.

Klaar om de AI-ontwikkelingskosten met 20% te verlagen?

Start gratis in enkele minuten. Gratis proeftegoeden inbegrepen. Geen creditcard vereist.

Lees Meer