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.
POST https://api.infro.io/v1/videosJob lifecycle
POST /v1/videosvalidates the request, checks your balance, and returns202 Acceptedwith a job inqueued.- INFRO selects a provider under your
routingpolicy and submits the render. The job moves torunningandprovideris filled in. - If a provider fails before producing output, the next provider — then each model in
fallbacks— is tried automatically. The job ID never changes. - The job reaches
succeededwithoutputandusage, orfailedwith anerror. Both states are terminal and immutable. - A
job.succeededorjob.failedwebhook 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, orluma/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, or10. Defaults to the model's shortest supported length. Billed per second of output. resolutionstring480p,720p(default), or1080p. Requesting a resolution a model cannot render returns400 invalid_request_errorrather than silently downgrading.aspect_ratiostring16:9(default),9:16, or1:1. Ignored whenimageis 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.
fallbacksmatters 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
{
"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
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
{
"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.