Skip to content
INFRO

Documentation

Video generation

Generate video with POST /v1/videos: the asynchronous job lifecycle, request parameters, polling and webhooks, and billing per second of output.


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}.

The submit call accepts the same universal extensions as every other endpoint — routing, fallbacks, logging — plus a per-request webhook. The job object itself, its fields, and its retention rules are documented in Async jobs.

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

Job lifecycle

  1. POST /v1/videos validates the request, checks your balance, and returns 202 Accepted with a job in queued.
  2. INFRO selects a provider under your routing policy and submits the render. The job moves to running and provider is filled in.
  3. If a provider fails before producing output, the next provider — then each model in fallbacks — is tried automatically. The job ID never changes.
  4. The job reaches succeeded with output and usage, or failed with an error. Both states are terminal and immutable.
  5. A job.succeeded or job.failed webhook fires if one is configured, and the output URL starts a 24-hour clock.

Request body

modelstringrequired
Video model ID, e.g. google/veo-3.1, kuaishou/kling-2.5, runway/gen-4, or luma/ray-3. See the catalog for supported durations and resolutions per model.
promptstringrequired
The scene to render. For image-to-video, describe the motion instead — camera move, subject action, pacing — since the first frame is already fixed.
duration_secondsinteger
Clip length. Models publish their own allowed values, commonly 5, 8, or 10. Defaults to the model's shortest supported length. Billed per second of output.
resolutionstring
480p, 720p (default), or 1080p. Requesting a resolution a model cannot render returns 400 invalid_request_error rather than silently downgrading.
aspect_ratiostring
16:9 (default), 9:16, or 1:1. Ignored when image is supplied and the model derives the ratio from the input frame.
imagestring
Image-to-video: a publicly reachable URL or a data: URI for the first frame. PNG, JPEG, or WebP up to 25 MB.
seedinteger
Best-effort deterministic sampling on providers that support it. Same seed, prompt, and model reproduce the same clip.
webhookobject
{"url": "https://example.com/hooks/infro", "events": ["job.succeeded", "job.failed"]}. Overrides the account endpoint for this job only. See Webhooks.
routing / fallbacks / loggingobject | array | boolean
The universal extensions, unchanged from chat completions. fallbacks matters more here than elsewhere: video capacity is the scarcest in the catalog. See Smart routing.

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",
    "fallbacks": ["kuaishou/kling-2.5"],
    "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",
  "provider": null,
  "created_at": 1755950000,
  "output": null,
  "usage": null,
  "error": null
}

provider is null until a provider accepts the work, and usage stays null until the job is terminal — the final price depends on the seconds actually rendered. Keep id; it is the only handle you need for everything that follows.

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 once, signed, with retries — no timers, no state machine of your own. Webhooks covers configuration and signature verification.

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",
            "fallbacks": ["kuaishou/kling-2.5"],
        },
        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"] != "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",
  "provider": "vertex",
  "created_at": 1755950000,
  "started_at": 1755950006,
  "completed_at": 1755950139,
  "expires_at": 1756036539,
  "output": {
    "url": "https://cdn.infro.io/vid/7d41ba90.mp4",
    "duration_seconds": 8,
    "resolution": "1080p",
    "aspect_ratio": "16:9",
    "mime_type": "video/mp4"
  },
  "usage": {
    "seconds": 8,
    "cost": 3.2
  }
}

output.url expires at expires_at, 24 hours after completion, and the file is deleted. Copy it to your own storage inside the webhook handler or right after the poll succeeds — a re-render costs full price.

Cost

Video is billed per second of output at the model's per-second rate, and the rate is higher at higher resolutions. usage.seconds is what was actually rendered, which can be marginally shorter than duration_seconds when a model rounds to whole frames; you are charged for usage.seconds, and usage.cost is the exact USD amount.

Failed and canceled jobs are never billed, including work that ran for minutes before failing. Rates are listed per model in the catalog and on pricing.

Canceling

POST /v1/jobs/{id}/cancel stops a queued or running job and drops the charge. Cancellation races with completion — a job that finished a moment earlier stays succeeded and is billed. Details in Async jobs, and error semantics in Errors.