Claude Opus 5 is now live on CometAPI →

Slik legger du til AI-videogenerering i en SaaS-app

CometAPI
AnnaJun 5, 2026
Slik legger du 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 møter dette første gang de kaller et video-API, venter på et svar med en video-URL, og får en oppgave-ID i stedet. Denne veiledningen går gjennom hele flyten: sende inn en oppgave, polle etter resultater, håndtere feil og lagre output før URL-en utløper.

Hva du skal bygge

En backend-tjeneste som godtar en tekstprompt eller et bilde, sender inn en videogenereringsoppgave, poller til den er fullført, og returnerer den endelige video-URL-en. Du jobber med fire modeller — Veo 3 Fast, Sora 2, Kling Video og Runway — alle gjennom én enkelt API-nøkkel.

Forutsetninger:

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

Forstå hvorfor videogenerering er annerledes

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 status-endepunkt hvert par sekunder
  3. Når statusen når en terminaltilstand, får du video-URL-en
  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 produksjonsnetttjeneste bør denne pollingsløyfen kjøre i en bakgrunnsarbeider (Celery, Bull eller lignende), ikke i request-handleren din. Eksemplene nedenfor bruker synkron polling — greit for skript og prototyper, men ikke for å håndtere samtidige brukere.

Velg en modell

ModelLeverandørMaks varighetPris (via CometAPI)Best egnet til
Veo 3 FastGoogle8 sek$0.05/sekRask prototyping, sosiale klipp
Sora 2OpenAI (via CometAPI-modell-ID)~10 sek$0.08/sekKreative kortvideoer 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

Kilde: CometAPI-modellsider, mai 2026. Merk: «Sora 2» er CometAPIs modell* identifikator — se deres modellsiden for detaljer om underliggende modell.

  • Veo 3 Fast støtter både tekst-til-video og bilde-til-video. Billigst per sekund, godt startpunkt.
  • Sora 2 genererer lyd nativt sammen med videoen — dialog, omgivelseslyd og effekter uten et separat 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 det.

Send inn en Veo-oppgave

Veo bruker multipart/form-data. Bruk files= i Python requests for å sende riktig — 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:    """Send inn en Veo 3 Fast tekst-til-video-oppgave. Returnerer task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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("En papirdrage som driver over en hveteåker på en vindfull ettermiddag")print(f"Oppgave sendt: {task_id}")

Poll etter resultatet

import time​def poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Poll til Veo-oppgaven er fullført. Returnerer video-URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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"Oppgave {task_id} feilet med status '{status}': "                f"{result.get('error', 'ingen feildetaljer returnert')}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Oppgave {task_id} ble ikke fullført innen {max_wait} sekunder")​​video_url = poll_veo_task(task_id)print(f"Video klar: {video_url}")

Bruk Kling Video for mer kontroll

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:    """Send inn en Kling tekst-til-video-oppgave. Returnerer task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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-oppgave til fullført. Returnerer video-URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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 bruker "succeed", ikke "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-oppgave {task_id} feilet: {error_detail}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Kling-oppgave {task_id} fikk tidsavbrudd etter {max_wait}s")

Kilde: CometAPI Kling Video-dokumentasjon

Animer et statisk bilde med 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:    """Send inn en Runway bilde-til-video-oppgave. Returnerer task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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,  # må være en stabil 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-oppgave. Returnerer video-URL når klar."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Miljøvariabelen COMETAPI_KEY er ikke satt")​    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-spesifikt: oppgaven initialiseres fortsatt, prøv igjen etter noen sekunder            time.sleep(interval)            elapsed += interval            continue        elif status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(f"Runway-oppgave {task_id} feilet: {result.get('error', 'no detail')}")​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Runway-oppgave {task_id} fikk tidsavbrudd etter {max_wait}s")

Kilde: CometAPI Runway-dokumentasjon

Lagre videoen før URL-en utløper

Video-URL-er 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:    """Last ned video fra URL til lokal fil med 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"Lagret til {output_path}")​​# Full flyttask_id = submit_veo_task("En timelapse av skyer som beveger seg over en byhorisont")video_url = poll_veo_task(task_id)download_video(video_url, "output/city_timelapse.mp4")

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

Håndter feil

SymptomSannsynlig årsakLøsning
Oppgave sitter fast i queued i 10+ minServerbelastning eller modellen er utilgjengeligPrøv på nytt med en annen modell
task_not_exist ved første Runway-pollOppgaven initialiseres fortsattVent 5 sek og prøv igjen — dokumentert CometAPI-oppførsel
feilet uten feilmeldingPrompten trigget innholdsfilterOmformuler prompten
Video-URL returnerer 403URL-en utløp før nedlastingLast ned umiddelbart etter at du får URL-en
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

Kilde: CometAPI videogenereringsdokumentasjon

Node.js-versjon

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

// Node.js 18+ — ingen ekstra pakker nødvendig​const API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY er ikke satt');​// --- 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-innsending feilet: ${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(`Polling feilet: ${res.status}`);    const result = await res.json();​    if (result.status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(result.status)) {      throw new Error(`Oppgave ${taskId} feilet: ${result.error ?? 'ingen detaljer'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Oppgave ${taskId} fikk tidsavbrudd`);}​// --- 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-innsending feilet: ${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 feilet: ${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-oppgave ${taskId} feilet: ${JSON.stringify(result.data.task_result ?? 'ingen detaljer')}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Kling-oppgave ${taskId} fikk tidsavbrudd`);}​// --- 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-innsending feilet: ${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 feilet: ${res.status}`);    const result = await res.json();    const status = result.status;​    if (status === 'task_not_exist') {      // CometAPI-spesifikt: oppgaven initialiseres fortsatt      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-oppgave ${taskId} feilet: ${result.error ?? 'ingen detaljer'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Runway-oppgave ${taskId} fikk tidsavbrudd`);}​// Brukseksempelconst taskId = await submitVeoTask('En papirdrage som driver over en hveteåker');const videoUrl = await pollVeoTask(taskId);console.log('Video klar:', videoUrl);

Hva er neste

Du har nå fungerende kode for fire videomodeller, en pollingsløyfe som håndterer feil, og et nedlastingssteg som gjør at du ikke mister generert innhold.

Neste problem 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 kan rute forespørsler på tvers av modeller uten å skrive om koden.

Neste: Hvordan bytte mellom KI-modeller uten å skrive om koden din

FAQ

Q: Hvorfor får jeg en oppgave-ID i stedet for en video i API-responsen?

Videogenerering er asynkron — modeller som Veo, Sora, Kling og Runway bruker 2–5 minutter på å rendere. API-et returnerer en oppgave-ID umiddelbart slik at forespørselen din ikke time’r ut. Du poller et separat status-endepunkt til oppgaven når en terminaltilstand (succeeded, succeed, failed).

Q: Hvor lenge er en generert video-URL gyldig?

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

Q: Hva er forskjellen mellom Veo 3 Fast og 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 å fininnstille output, bruk Kling. Trenger du fart og lav kost, bruk Veo 3 Fast.

Q: Kan jeg generere video fra et bilde i stedet for en tekstprompt?

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 godtar ikke rene tekstprompter via CometAPI.

Q: Hvorfor returnerer Runway task_not_exist ved første poll?

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

Q: Hvorfor bruker Kling "succeed" i stedet for "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 poller, må du håndtere begge strenger.

Q: Er den synkrone pollingsløyfen trygg å bruke i en webserver?

Nei. Pollingsløyfen i denne veiledningen blokkerer tråden i flere minutter. 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-en 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