Skip to content

DocsModalities

Video generation

Generate video with POST /v1/videos: the asynchronous job lifecycle, request parameters, polling and webhooks, and provider cost estimates for generated output.


INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See Early access.

Video renders take between twenty seconds and several minutes, far longer than an HTTP request should stay open, so /v1/videos is asynchronous. Submitting returns a job immediately; the finished video arrives through a webhook or a poll of GET /v1/jobs/{id} (GET /v1/videos/{id} returns the same job object; the SDKs poll that path).

The submit call accepts the logging and metadata extensions plus a per-request webhook. The job object itself, its fields, and its retention rules are documented in Async jobs.

Video is served by connections to Google AI (Gemini), xAI, OpenRouter, Runway, KIE, LaoZhang API, MiniMax, Alibaba Cloud Model Studio, fal.ai, Replicate and Sync, each on your own account with that provider. The endpoint speaks OpenAI's Videos protocol (submit, poll, then read the finished file's URL) and INFRO translates it to each provider's own API. When none of your connections serves the requested model, the request answers 503 no_available_provider.

Endpoint
POST https://api.infro.io/v1/videos

Job lifecycle

  1. POST /v1/videos validates the request, checks it against your budgets using the requested duration, and returns 202 Accepted with a job in queued.
  2. INFRO submits the render to the first connection that accepts it, trying the next target on your route if a submit fails. The job moves to running and route, provider, and connection are filled in.
  3. INFRO then asks the provider for the render's status: first about 15 seconds after submitting, then every one to five minutes. Once a render is running it is not moved to another connection; if it fails, the job fails.
  4. The job reaches succeeded with output and usage, or failed with an error. A render still unfinished after 60 status checks (roughly four and a half hours) fails with 408 request_timeout. Terminal states do not change.
  5. A job.succeeded or job.failed webhook is queued if one is configured, and the output URL starts a 24-hour clock.

Request body

modelstringrequired
Video model ID, e.g. google/gemini-omni-flash, google/veo-3.1, kuaishou/kling-o3, runway/gen-4.5, or luma/ray-3.2, or the slug of one of your routes. Each model renders its own durations, resolutions and aspect ratios on each provider; INFRO checks them before any job is created (see each parameter below).
promptstringrequired
The scene to render, up to 32,000 characters. For image-to-video, describe the motion instead — camera move, subject action, pacing — since the first frame is already fixed.
duration_secondsinteger
Clip length in seconds. Each model renders its own lengths on each provider (Veo 3.1 4, 6 or 8; Seedance 2.5 4 to 30), and a length the target cannot render is refused before it is called. Omitted, the provider's default applies. Providers bill per second of output.
resolutionstring
360p, 480p, 540p, 720p, 768p, 1080p, 1440p, 2K or 4K, as the model takes them on each provider. Some are reached at some lengths only: Veo renders 1080p and 4K at 8 seconds. Omitted, the provider's default applies.
aspect_ratiostring
W:H, e.g. 16:9, 9:16, 1:1, 4:3 or 21:9, as the model takes them. Omitted, the provider's default applies.
generate_audioboolean
Generated audio on or off, where the model has a switch (Seedance, Veo on fal and Replicate, Kling, PixVerse, Wan 3.0). Asking a model that always renders with audio (Veo on the Gemini API) for none is refused. Omitted, the model's default applies; audio can change the price.
imagestring
Image-to-video: a URL or a data: URI for the first frame, forwarded to the provider as its input reference. Up to 10 million characters; the request body is limited to 11 MB.
seedinteger
Best-effort deterministic sampling, sent only to providers whose model takes a seed; elsewhere it is dropped.
webhookobject
{"url": "https://example.com/hooks/infro", "events": ["job.succeeded", "job.failed"]}. Replaces your account endpoints for this job only. events defaults to all three job events. See Webhooks — deliveries to a per-request URL are not signed.
logging / metadataboolean | object
The universal extensions, unchanged from chat completions. fallbacks works for video too: the list is kept with the job, and when the render is submitted INFRO tries the targets for model first, then each fallback in order, so a fallback must be a video model. An ID that is not a model this organization can call is refused with 400 unknown_fallback_model when the job is created. A render that cannot be submitted on any of them fails, and nothing is charged. Once a provider has accepted the render it is never moved to another one. routing.policy is accepted and ignored.

Last frame, negative prompt, references, extend and video-to-video

Checked against what the model takes on each target, like every setting; a target that cannot take one is skipped, and when none can the request is a 400.

last_framestring
The frame the video ends on: an https URL or an image data: URI. Needs image (the first frame).
negative_promptstring
What the render should avoid, up to 4,000 characters.
referencesarray
Reference images, { source, role } each, as on images. Some models fix other settings when they get references: Veo 3.1 renders 8 seconds; on Runway, references and a first frame cannot be sent together.
extendobject
{ "job_id": "job_…" }: continue the finished video of an earlier job of this organization, on a model and provider that extend.
videostring
An https URL of a video to restyle or edit (video-to-video), on a model and provider that take one. The provider fetches it.

Upscale and lip-sync

Two more kinds of job, polled and webhooked exactly like a render, priced per second of output:

POST /v1/videos/upscalejob
{ model, video | video_job_id, scale | resolution }: a video, larger. video is an https URL the provider fetches; video_job_id names an earlier finished render of this organization. Topaz takes a factor; Magnific on Runway a target resolution (720p, 1080p, 2K, 4K).
POST /v1/videos/lipsyncjob
{ model, video | image, audio }: a video, or a still image on models that animate one (Sync 3, OmniHuman, Kling AI Avatar), moved to the speech in audio. Inputs are https URLs, or data: URIs where the provider takes them; Sync and fal fetch URLs only. Sync is its own provider kind: connect your Sync API key.

Submitting a job

curl https://api.infro.io/v1/videos \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/veo-3.1",
    "prompt": "A paper boat drifting down a rain-slicked gutter, macro lens",
    "duration_seconds": 8,
    "resolution": "1080p",
    "aspect_ratio": "16:9",
    "webhook": {
      "url": "https://example.com/hooks/infro",
      "events": ["job.succeeded", "job.failed"]
    }
  }'

The 202 response

json
{
  "id": "job_7d41ba90",
  "object": "job",
  "type": "video.generation",
  "status": "queued",
  "model": "google/veo-3.1",
  "route": null,
  "provider": null,
  "connection": null,
  "progress": 0,
  "created_at": 1755950000,
  "started_at": null,
  "completed_at": null,
  "expires_at": null,
  "output": null,
  "usage": null,
  "error": null
}

route, provider, and connection are null until the render is submitted, and usage stays null until the job succeeds — the estimate depends on the seconds actually rendered. Keep id; it is the only handle you need for everything that follows. Resending the same request with the same Idempotency-Key returns this same 202 body rather than starting a second render.

Polling vs webhooks

Use webhooks in anything long-lived: a server, a queue worker, a scheduled function. INFRO delivers the terminal job to your endpoint, signed, with retries — no timers, no state machine of your own. Webhooks covers configuration and signature verification.

Either way, INFRO learns a render finished by asking the provider on the schedule above, so a result can reach you a few minutes after the provider completes it. Polling INFRO faster than that does not make it arrive sooner.

Poll when you have nowhere to receive a callback: a CLI, a notebook, a mobile app doing a one-off render. Poll on exponential backoff, never in a tight loop, and always with a deadline of your own.

A two-minute render polled once a second is 120 requests that all count against your rate limits. The same render with a webhook is one delivery.

Worked example: submit, then poll

render.py
import os
import time

import requests

BASE_URL = "https://api.infro.io/v1"
API_KEY = os.environ["INFRO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
TERMINAL = {"succeeded", "failed", "canceled"}


def submit(prompt: str) -> str:
    res = requests.post(
        f"{BASE_URL}/videos",
        headers=HEADERS,
        json={
            "model": "google/veo-3.1",
            "prompt": prompt,
            "duration_seconds": 8,
            "resolution": "1080p",
        },
        timeout=30,
    )
    res.raise_for_status()
    return res.json()["id"]


def wait(job_id: str, timeout_s: int = 900) -> dict:
    delay = 2.0
    deadline = time.time() + timeout_s

    while time.time() < deadline:
        res = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=HEADERS, timeout=30)
        res.raise_for_status()
        job = res.json()

        if job["status"] in TERMINAL:
            return job

        time.sleep(delay)
        delay = min(delay * 1.5, 30.0)  # back off, cap at 30s

    raise TimeoutError(f"{job_id} did not finish within {timeout_s}s")


job = wait(submit("A paper boat drifting down a rain-slicked gutter, macro lens"))

if job["status"] == "canceled":
    raise RuntimeError("render was canceled")
if job["status"] != "succeeded":
    raise RuntimeError(job["error"]["message"])

print(job["output"]["url"], job["usage"]["cost"])

Terminal job payload

json
{
  "id": "job_7d41ba90",
  "object": "job",
  "type": "video.generation",
  "status": "succeeded",
  "model": "google/veo-3.1",
  "route": "primary",
  "provider": "openai-compatible",
  "connection": "conn_8b21c4e0",
  "progress": 1,
  "created_at": 1755950000,
  "started_at": 1755950006,
  "completed_at": 1755950139,
  "expires_at": 1756036539,
  "output": {
    "url": "https://api.infro.io/media/vid/2026-09-24/org_.../job_7d41ba90.mp4?exp=1756036539&sig=...",
    "duration_seconds": 8,
    "resolution": "1080p",
    "aspect_ratio": "16:9",
    "mime_type": "video/mp4"
  },
  "usage": {
    "seconds": 8,
    "cost": 3.2,
    "price_source": "list"
  },
  "error": null
}

output.url expires at expires_at, 24 hours after completion, and INFRO then deletes its copy of the file. Copy it to your own storage inside the webhook handler or right after the poll succeeds — a re-render is billed again by your provider.

Cost

Providers bill video per second of output, and the rate is usually higher at higher resolutions. usage.seconds is the duration the provider reports for the finished file; usage.cost estimates what your provider will bill for it, and is null when the model is unpriced.

INFRO records no cost for failed and canceled jobs. Whether your provider bills a render that failed or was canceled is up to the provider. Vendor list rates are in the catalog.

Canceling

POST /v1/jobs/{id}/cancel marks a queued or running job canceled and stops INFRO polling it. It does not currently cancel the render at your provider, which may finish it and bill you for it. Cancellation races with completion — a job that finished a moment earlier stays succeeded. Details in Async jobs, and error semantics in Errors.

↑ ↓ navigate↵ open/ anywhere in docs