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

Hvordan legge til AI-videogenerering i en SaaS-app

CometAPI
AnnaJun 5, 2026
Hvordan legge til AI-videogenerering i en SaaS-app

Å legge til videogenerering i appen din er ikke det samme som å legge til bildegenerering. API-kallet returnerer umiddelbart — men videoen er ikke klar ennå. Du får en oppgave-ID, og du må fortsette å spørre «er den ferdig?» til den er det.

De fleste utviklere støter på dette første gang de kaller et video-API, venter på et svar med en videolenke, og får tilbake en oppgave-ID i stedet. Denne veiledningen går gjennom hele flyten: sende inn en oppgave, polle etter resultater, håndtere feil og lagre utdataene før URL-en utløper.

What you'll build

En backend-tjeneste som aksepterer en tekstprompt eller et bilde, sender inn en videogenereringsoppgave, polle til den er fullført, og returnerer den endelige videolenken. Du vil jobbe med fire modeller — Veo 3 Fast, Sora 2, Kling Video og Runway — alle via én enkelt API-nøkkel.

Prerequisites:

  • Python 3.8+ eller Node.js 18+
  • En CometAPI-nøkkel
  • Grunnleggende kjennskap til REST API-er

Understand why video generation is different

Med bildegenerering sender du en forespørsel og får bildet tilbake i samme respons. Videogenerering bruker en asynkron oppgavekø:

  1. Send inn en genereringsforespørsel → få tilbake en task_id
  2. Poll et statusendepunkt hvert noen sekunder
  3. Når status når en terminal tilstand, får du videolenken
  4. Last ned og lagre videoen — URL-en er midlertidig

Hvis du behandler videogenerering som bildegenerering og venter på at den første responsen skal inneholde videoen, vil forespørselen din time ut hver gang.

I en produksjonswebtjeneste bør denne polling‑løkka kjøre i en bakgrunnsarbeider (Celery, Bull eller lignende), ikke i request-handleren din. Eksemplene nedenfor bruker synkron polling — fint for skript og prototyper, men ikke for å håndtere samtidige brukere.

Choose a model

ModelProviderMax durationPrice (via CometAPI)Best for
Veo 3 FastGoogle8 sek$0.05/sekRask prototyping, sosiale klipp
Sora 2OpenAI (via CometAPI model ID)~10 sek$0.08/sekKreative kortfilmer i høy kvalitet
Kling VideoKuaishou10 sek$0.13–$2.64/oppgaveMarkedsføringsinnhold, finstyring
Runway Gen-3A TurboRunway5 eller 10 sek$0.32/oppgaveBilde-til-video, kommersielt innhold

Source**: CometAPI model pages, mai 2026. Note: "Sora 2" is CometAPI's model identifier — refer to their model page for the underlying model details.

  • Veo 3 Fast støtter både tekst-til-video og bilde-til-video. Billigst per sekund, et godt startpunkt.
  • Sora 2 genererer lyd nativt sammen med videoen — dialog, omgivelseslyd og effekter uten et eget TTS-trinn.
  • Kling Video gir deg negative_prompt, cfg_scale, kamerabevegelsesinnstillinger og en pro-modus. Mest kontroll av de fire.
  • Runway er kun bilde-til-video via CometAPI. Gi den et statisk bilde og en bevegelsesbeskrivelse, så animerer den bildet.

Submit a Veo task

Veo bruker multipart/form-data. Bruk files= i Python requests for å sende korrekt — data=dict sender application/x-www-form-urlencoded, som ikke er det samme:

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}")

Poll for the result

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}")

Use Kling Video for more control

Kling har en annen endepunktstruktur og bruker JSON. Merk at Klings terminale statusstreng er "succeed" (ikke "succeeded") — dette samsvarer med API-ets faktiske responsformat:

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")

Source**: CometAPI Kling Video docs

Animate a static image with Runway

Runway er kun bilde-til-video. Den krever også en ekstra 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")

Source**: CometAPI Runway docs

Save the video before the URL expires

Videolenker fra genererings-API-er er midlertidige. Last ned filen umiddelbart og lagre den et sted du kontrollerer:

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")

I produksjon, bytt ut lokal filskriving med opplasting til S3, Cloudflare R2 eller lagringen du foretrekker. Streaming-mønsteret er det samme — send bytene direkte i stedet for å laste hele videoen i minnet.

Handle failures

SymptomLikely causeFix
Oppgave fast i queued i 10+ minServerlast eller modell utilgjengeligPrøv igjen med en annen modell
task_not_exist ved første Runway-pollOppgaven initialiseres fortsattVent 5 sek og prøv igjen — dokumentert CometAPI-atferd
failed uten feilmeldingPrompt utløste innholdsfilterFormuler prompten på nytt
Videolenke returnerer 403URL utløp før nedlastingLast ned umiddelbart etter at du får lenken
Tidsavbrudd etter 10 minGenereringen tok for lang tidØk max_wait eller bytt til Veo 3 Fast
Kling returnerer "succeed" ikke "succeeded"Klings API bruker en ikke-standard statusstrengDette er korrekt — se Kling‑pollingkoden over

Source: CometAPI video generation docs

Node.js version

Node.js 18+ inkluderer fetch og FormData nativt. Dette eksemplet dekker alle fire modellene:

// 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);

What's next

Du har nå fungerende kode for fire videomodeller, en polling‑løkke som håndterer feil, og et nedlastingssteg som hindrer at du mister generert innhold.

Det neste problemet de fleste utviklere møter: de har hardkodet én modell, og å bytte til et billigere eller raskere alternativ betyr å endre flere filer. Den neste artikkelen dekker hvordan du ruter forespørsler på tvers av modeller uten å skrive om koden din.

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

FAQ

Q: Why do I get a task ID instead of a video in the API response?

Videogenerering er asynkron — modeller som Veo, Sora, Kling og Runway bruker 2–5 minutter på rendering. API-et returnerer en oppgave-ID umiddelbart slik at forespørselen din ikke timer ut. Du poller et eget statusendepunkt til oppgaven når en terminal tilstand (succeeded, succeed, failed).

Q: How long does a generated video URL stay valid?

Videolenker fra genererings-API-er er midlertidige. Last ned filen umiddelbart etter at du får lenken og lagre den i din egen lagring (S3, Cloudflare R2, osv.). Ikke lagre URL-en og forvent at den fungerer flere timer senere.

Q: What's the difference between Veo 3 Fast and Kling Video?

Veo 3 Fast er billigere ($0.05/sek), raskere og enklere å kalle. Kling Video gir deg mer kontroll: negative_prompt, cfg_scale, kamerabevegelsesinnstillinger og en pro‑kvalitetsmodus. Trenger du å finjustere resultatet, bruk Kling. Trenger du hastighet og lav kostnad, bruk Veo 3 Fast.

Q: Can I generate video from an image instead of a text prompt?

Ja. Veo støtter bilde-til-video ved å sende en input_reference‑fil. Kling støtter det via endepunktet /kling/v1/videos/image2video med en image‑parameter (URL eller base64). Runway er kun bilde-til-video — den aksepterer ikke tekst‑kun‑prompter via CometAPI.

Q: Why does Runway return task_not_exist on the first poll?

Dette er dokumentert CometAPI‑atferd — oppgaven initialiseres fortsatt i backend. Vent noen sekunder og prøv igjen. Det er ikke en feil. Polling-koden over håndterer dette automatisk.

Q: Why does Kling use "succeed" instead of "succeeded"?

Det er Klings faktiske API‑responsformat. Det er ikke en skrivefeil. Veo og Runway bruker "succeeded" — Kling bruker "succeed". Hvis du bygger en samlet polling‑innpakning, må du håndtere begge strenger.

Q: Is the synchronous polling loop safe to use in a web server?

Nei. Polling‑løkka i denne veiledningen blokkerer tråden i flere minutter om gangen. I en ekte webtjeneste med samtidige brukere, kjør pollingen i en bakgrunnsarbeider (Celery for Python, Bull for Node.js). Send inn oppgaven i request-handleren, returner oppgave‑ID til klienten, og la arbeideren varsle klienten når videoen er klar.

Klar til å redusere AI-utviklingskostnadene med 20 %?

Kom i gang gratis på minutter. Gratis prøvekreditter inkludert. Ingen kredittkort nødvendig.

Les mer