Claude Fable 5 is now on CometAPI — state-of-the-art performance in coding, agents, and scientific research. Try it now

Hoe AI-videogeneratie aan een SaaS-app toevoegen

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

Het toevoegen van videogeneratie aan je app is niet hetzelfde als het toevoegen van afbeeldingsgeneratie. De API-call keert direct terug — maar de video is dan nog niet klaar. Je krijgt een task-ID en moet blijven vragen “is het al klaar?” totdat dat zo 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 task-ID terugkrijgen. Deze gids doorloopt de volledige flow: een taak indienen, resultaten pollend opvragen, fouten afhandelen en de output opslaan voordat de URL verloopt.

Wat je gaat bouwen

Een backend-service die een tekstprompt of afbeelding accepteert, een videogeneratietaak indient, blijft pollen totdat deze is voltooid en de uiteindelijke video-URL retourneert. Je werkt met vier modellen — Veo 3 Fast, Sora 2, Kling Video en Runway — allemaal met één API-sleutel.

Prerequisites:

  • 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 generatietaak → je krijgt een task_id terug
  2. Een status-endpoint elke paar seconden pollen
  3. Wanneer de status een terminale toestand bereikt, krijg je de video-URL
  4. Download en sla de video 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 moet deze pollingloop in een achtergrondworker draaien (Celery, Bull of iets dergelijks), niet in je request-handler. De voorbeelden hieronder gebruiken synchrone polling — prima voor scripts en prototypes, maar niet voor het verwerken van gelijktijdige gebruikers.

Kies een model

ModelProviderMaximale duurPrijs (via CometAPI)Bijzonder geschikt voor
Veo 3 FastGoogle8 sec$0.05/secSnelle prototyping, social clips
Sora 2OpenAI (via CometAPI model ID)~10 sec$0.08/secCreatieve shorts van hoge kwaliteit
Kling VideoKuaishou10 sec$0.13–$2.64/taskMarketingcontent, fijne 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 modelidentifier — raadpleeg hun modelpagina voor details over het onderliggende model.

  • Veo 3 Fast ondersteunt zowel text-to-video als image-to-video. Het goedkoopst per seconde, een goed startpunt.
  • Sora 2 genereert audio native naast de video — dialoog, omgevingsgeluid en effecten zonder aparte TTS-stap.
  • Kling Video biedt je negative_prompt, cfg_scale, camerabewegingsinstellingen 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 deze.

Dien een Veo-taak in

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

import requestsimport osfrom dotenv import load_dotenv​load_dotenv()​def submit_veo_task(prompt: str, size: str = "16x9") -> str:    """Dien een Veo 3 Fast text-to-video-taak in. Retourneert task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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("Een papieren vlieger die op een winderige middag boven een korenveld zweeft")print(f"Taak ingediend: {task_id}")

Poll voor het resultaat

import time​def poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Poll totdat de Veo-taak voltooid is. Geeft de video-URL terug."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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"Taak {task_id} is mislukt met status '{status}': "                f"{result.get('error', 'geen foutdetails ontvangen')}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Taak {task_id} is niet voltooid binnen {max_wait} seconden")​​video_url = poll_veo_task(task_id)print(f"Video klaar: {video_url}")

Gebruik Kling Video 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 de daadwerkelijke API-respons:

def submit_kling_task(prompt: str, duration: str = "5", mode: str = "std") -> str:    """Dien een Kling text-to-video-taak in. Retourneert task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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" of "pro"            "aspect_ratio": "16:9",            "duration": duration  # "5" of "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-taak tot voltooid. Geeft de video-URL terug."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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 gebruikt "succeed", niet "succeeded"            return result["data"]["task_result"]["videos"][0]["url"]        elif status == "failed":            error_detail = result.get("data", {}).get("task_result", "geen details")            raise RuntimeError(                f"Kling-taak {task_id} is mislukt: {error_detail}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Kling-taak {task_id} heeft een time-out na {max_wait}s")

Bron: CometAPI-documentatie voor Kling Video

Animeer een statische afbeelding 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:    """Dien een Runway image-to-video-taak in. Retourneert task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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,  # moet een stabiele HTTPS-URL zijn            "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-taak. Geeft de video-URL terug wanneer klaar."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Omgevingsvariabele COMETAPI_KEY is niet ingesteld")​    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-specifiek: taak is nog aan het initialiseren, probeer na enkele seconden opnieuw            time.sleep(interval)            elapsed += interval            continue        elif status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(f"Runway-taak {task_id} is mislukt: {result.get('error', 'geen details')}")​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Runway-taak {task_id} heeft een time-out na {max_wait}s")

Bron: CometAPI-documentatie voor Runway

Sla de video op voordat de URL verloopt

Video-URL’s van generatieve API’s zijn tijdelijk. Download het bestand meteen en sla het ergens op dat jij beheert:

import requestsimport pathlib​def download_video(url: str, output_path: str) -> None:    """Download video van URL naar lokaal bestand via 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"Opgeslagen naar {output_path}")​​# Volledige flowtask_id = submit_veo_task("Een timelapse van wolken die over een stads-skyline bewegen")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 je 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 10+ min in queuedServerload of model niet beschikbaarProbeer opnieuw met een ander model
task_not_exist bij eerste Runway-pollTaak is nog aan het initialiserenWacht 5 sec en probeer opnieuw — gedocumenteerd gedrag
failed zonder foutmeldingPrompt triggert contentfilterHerschrijf de prompt
Video-URL geeft 403URL verlopen vóór downloadDownload onmiddellijk nadat je de URL krijgt
Time-out na 10 minGeneratie duurde te langVerhoog max_wait of schakel over op Veo 3 Fast
Kling geeft "succeed" i.p.v. "succeeded"Kling’s API gebruikt niet-standaard statusstringDit is correct — zie de Kling-pollingcode hierboven

Bron: CometAPI-documentatie over videogeneratie

Node.js-versie

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

// Node.js 18+ — geen extra pakketten nodig​const API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY is niet ingesteld');​// --- 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-indiening mislukt: ${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(`Pollen mislukt: ${res.status}`);    const result = await res.json();​    if (result.status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(result.status)) {      throw new Error(`Taak ${taskId} is mislukt: ${result.error ?? 'geen details'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Taak ${taskId} heeft een time-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-indiening mislukt: ${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-polling mislukt: ${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-taak ${taskId} is mislukt: ${JSON.stringify(result.data.task_result ?? 'geen details')}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Kling-taak ${taskId} heeft een time-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-indiening mislukt: ${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-polling mislukt: ${res.status}`);    const result = await res.json();    const status = result.status;​    if (status === 'task_not_exist') {      // CometAPI-specifiek: taak is nog aan het initialiseren      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-taak ${taskId} is mislukt: ${result.error ?? 'geen details'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Runway-taak ${taskId} heeft een time-out`);}​// Voorbeeldgebruikconst taskId = await submitVeoTask('Een papieren vlieger die boven een korenveld zweeft');const videoUrl = await pollVeoTask(taskId);console.log('Video klaar:', videoUrl);

Wat nu

Je hebt nu werkende code voor vier videomodellen, een pollingloop die fouten afhandelt en een downloadstap die voorkomt dat je gegenereerde content verliest.

Het volgende probleem waar de meeste ontwikkelaars tegenaan lopen: ze hebben één model hardcoded, en overschakelen naar een goedkoper of sneller alternatief betekent meerdere bestanden aanpassen. Het volgende artikel behandelt hoe je requests over modellen kunt routeren zonder je code te herschrijven.

Volgende: How to Switch Between AI Models Without Rewriting Your Code

FAQ

Vraag: Waarom krijg ik een task-ID in plaats van een video in de API-respons?

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

Vraag: Hoe lang blijft een gegenereerde video-URL geldig?

Video-URL’s van generatieve API’s zijn tijdelijk. Download het bestand direct nadat je de URL krijgt 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.

Vraag: 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, camerabewegingen en een pro-kwaliteitsmodus. Als je de output fijn wilt afstellen, gebruik Kling. Heb je snelheid en lage kosten nodig, gebruik Veo 3 Fast.

Vraag: 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 het via het /kling/v1/videos/image2video-endpoint met een image-parameter (URL of base64). Runway is uitsluitend image-to-video — het accepteert geen uitsluitend tekstprompts via CometAPI.

Vraag: Waarom geeft Runway task_not_exist bij de eerste poll?

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

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

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

Vraag: Is de synchrone pollingloop veilig om te gebruiken in een webserver?

Nee. De pollingloop in deze gids blokkeert de thread minutenlang. In een echte webservice met gelijktijdige gebruikers laat je de polling in een achtergrondworker draaien (Celery voor Python, Bull voor Node.js). Dien de taak in de request-handler in, retourneer de task-ID aan 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