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.
POST https://api.infro.io/v1/videosJob lifecycle
POST /v1/videosvalidates the request, checks it against your budgets using the requested duration, and returns202 Acceptedwith a job inqueued.- 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
runningandroute,provider, andconnectionare filled in. - 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.
- The job reaches
succeededwithoutputandusage, orfailedwith anerror. A render still unfinished after 60 status checks (roughly four and a half hours) fails with408 request_timeout. Terminal states do not change. - A
job.succeededorjob.failedwebhook 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, orluma/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,6or8; Seedance 2.54to30), 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. resolutionstring360p,480p,540p,720p,768p,1080p,1440p,2Kor4K, as the model takes them on each provider. Some are reached at some lengths only: Veo renders1080pand4Kat 8 seconds. Omitted, the provider's default applies.aspect_ratiostringW:H, e.g.16:9,9:16,1:1,4:3or21: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.eventsdefaults 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.
fallbacksworks for video too: the list is kept with the job, and when the render is submitted INFRO tries the targets formodelfirst, 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 with400 unknown_fallback_modelwhen 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.policyis 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.videois an https URL the provider fetches;video_job_idnames 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 inaudio. Inputs are https URLs, ordata: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"]
}
}'import os
import requests
BASE_URL = "https://api.infro.io/v1"
API_KEY = os.environ["INFRO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
res = requests.post(
f"{BASE_URL}/videos",
headers=HEADERS,
json={
"model": "google/veo-3.1",
"prompt": "A paper boat drifting down a rain-slicked gutter, macro lens",
"duration_seconds": 8,
"resolution": "1080p",
"webhook": {
"url": "https://example.com/hooks/infro",
"events": ["job.succeeded", "job.failed"],
},
},
timeout=30,
)
res.raise_for_status()
job = res.json()
print(job["id"], job["status"]) # job_7d41ba90 queuedconst res = await fetch("https://api.infro.io/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/veo-3.1",
prompt: "A paper boat drifting down a rain-slicked gutter, macro lens",
duration_seconds: 8,
resolution: "1080p",
webhook: {
url: "https://example.com/hooks/infro",
events: ["job.succeeded", "job.failed"],
},
}),
});
if (!res.ok) throw new Error(await res.text());
const job = await res.json();
console.log(job.id, job.status); // job_7d41ba90 queuedimport 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
Future<void> main() async {
final apiKey = Platform.environment['INFRO_API_KEY']!;
final res = await http.post(
Uri.parse('https://api.infro.io/v1/videos'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({
'model': 'google/veo-3.1',
'prompt': 'A paper boat drifting down a rain-slicked gutter, macro lens',
'duration_seconds': 8,
'resolution': '1080p',
'webhook': {
'url': 'https://example.com/hooks/infro',
'events': ['job.succeeded', 'job.failed'],
},
}),
);
final job = jsonDecode(res.body) as Map<String, dynamic>;
print(job['id']);
print(job['status']);
}The 202 response
{
"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
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
{
"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.