Calling Veo 3.1, Fast and Lite from curl and Python
Updated 2026-10-02
Veo 3.1 is available through one endpoint, POST https://videorouter.sh/api/v1/videos, under three model ids: google/veo-3.1, google/veo-3.1-fast and google/veo-3.1-lite. You need a VideoRouter key, not a cloud project. This page covers the working request, the polling loop, host pinning, and the errors you should handle on day one.
Create a job
curl https://videorouter.sh/api/v1/videos \
-H "Authorization: Bearer llmr_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"model": "google/veo-3.1-fast",
"prompt": "a lighthouse beam sweeping across a foggy harbor at night",
"duration_secs": 8,
"aspect_ratio": "16:9",
"resolution": "720p"
}'
The response is a job, not a video: an id, status: "queued" and the provider that accepted it. The full cost is charged once here, from the requested duration, plus a 2% platform fee on video usage. Everything after this call is free.
Poll and download in Python
import time
import requests
BASE = "https://videorouter.sh/api/v1"
H = {"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"}
def veo(prompt, model="google/veo-3.1-fast", **fields):
r = requests.post(f"{BASE}/videos", headers=H,
json={"model": model, "prompt": prompt, **fields})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 5)))
r = requests.post(f"{BASE}/videos", headers=H,
json={"model": model, "prompt": prompt, **fields})
r.raise_for_status()
job = r.json()
deadline = time.time() + 900
while job["status"] not in ("completed", "failed"):
if time.time() > deadline:
raise TimeoutError(f"{job['id']} still running; poll it, do not resubmit")
time.sleep(5)
job = requests.get(f"{BASE}/videos/{job['id']}", headers=H).json()
if job["status"] == "failed":
raise RuntimeError(job["error"])
return job["data"][0]["url"]
print(veo("a lighthouse beam sweeping across a foggy harbor", duration_secs=8))
Status goes queued, in_progress, then completed or failed. On success the URL is at data[0].url; copy the file to your own storage rather than keeping that link. Jobs that fail upstream are not billed. Never resubmit a slow job: the second submission is a second charge, while polling is free.
Switching between Veo tiers
All three ids share one request shape, so promoting a prompt from draft to final is a string change: google/veo-3.1-lite for exploration, google/veo-3.1-fast for review, google/veo-3.1 for approved shots. Resolution support is not identical across tiers, and for Fast the resolution tier also changes the price, so look at each model page before hard-coding a tier. The cost strategy guide covers how to sequence them.
Choosing and pinning a host
Veo 3.1 is sold by several hosts, Google among them, and rates differ between them. Unpinned requests go to the cheapest healthy host, and a rejected submission falls through to the next one. For the flagship, the confirmed host suffixes in VideoRouter's documentation are google, pika, deepinfra, machgen, replicate and wavespeed; check the host table on each tier's model page for its own list.
"model": "google/veo-3.1-fast/replicate"
That suffix is a soft preference. To fail instead of falling back, send:
"provider": {"only": ["replicate"], "allow_fallbacks": false}
Pin when a host has a capability others lack, such as image input, or when you have a policy reason to use a named provider. Otherwise float and keep failover. Calling Google directly is not automatically the cheap option; compare before assuming:
| Model | Cheapest host | Priciest host | Cheapest is | Hosts |
|---|---|---|---|---|
| google/veo-3.1 (2160p) | Pika $0.2 / second | MachGen $0.6 / second | 67% lower | 7 |
| google/veo-3.1-fast (2160p) | Replicate $0.1 / second | MachGen $0.3 / second | 67% lower | 7 |
| google/veo-3.1-lite (1080p) | SandBase $0.03 / second | WaveSpeedAI $0.08 / second | 62% lower | 3 |
Per second, before VideoRouter's 2% platform fee. For tiered models each row compares the resolution tier with the widest host-to-host gap. Built 2026-10-02 from the live catalog.
Handling errors
Every error uses the OpenAI-style envelope {"error": {"message", "type", "code"}} whichever host was involved.
| Status | Typical cause | Response |
|---|---|---|
400 invalid_request_error | Missing prompt, a typo in the model id, or a field the model or host does not accept | Fix the request; do not retry |
401 invalid_api_key | Missing, revoked or expired key | Check the key |
| 402 | Monthly key cap reached or prepaid balance exhausted | Top up or raise the cap |
403 model_not_allowed | The tier is not in the key's allow-list | Update the allow-list |
| 429 | Per-key rate limit | Sleep for Retry-After and retry |
5xx upstream_error | Every candidate host failed | Retry; not billed |
There is a second failure channel: a job accepted successfully that later ends with status: "failed". Content-policy rejections are an ordinary outcome for prompts and user-supplied images, so show those to the user rather than retrying blindly. For a job that stalls after acceptance, failover.on_timeout_sec hedges to a second host, but both attempts are billed if both finish.
Testing without burning budget
Before you wire Veo into a product, run a small matrix of short requests: one per tier you plan to use, one with and one without your image, each with the resolution and aspect ratio you intend to ship. Keep the request body next to each job id and inspect each output. Because billing happens at creation, a ten-request test costs ten short clips, and a single misread default found now is cheaper than the same mistake across a batch. Give the key a monthly cap and a model allow-list while testing so a runaway loop cannot spend more than you intended.
Pitfalls worth testing for
- Silent defaults. An unsupported
resolutionoraspect_rationever returns a 400; it is ignored, and some Veo host rows ignore both fields entirely. Check the returned file's dimensions. - Image input is host-specific. A host that does not support it rejects
start_image_urlwith a 400; see the image-to-video guide. - Duration snapping.
duration_secsis snapped to a value the model supports, and billing follows the snapped value.
Next, read the Veo 3.1 parameters page, browse the model pages for host tables, or create a key and run the curl above. The quickstart has the same flow end to end.
Frequently asked questions
What are the model ids for Veo 3.1?
google/veo-3.1, google/veo-3.1-fast and google/veo-3.1-lite. They share one request shape, so switching tiers means changing the model string.
Do I need a Google Cloud account?
No. A VideoRouter API key against https://videorouter.sh/api/v1/videos is enough. Google is one host among several the request can be routed to.
How do I pin Veo to a specific host?
Add a suffix such as google/veo-3.1-fast/replicate for a soft preference, or use provider.only with allow_fallbacks: false for a hard pin with no fallback.
Am I billed when a Veo job fails?
Jobs that fail upstream are not billed. A successful job is billed once at creation from the requested duration, and polling is free.
Keep reading
- Veo 3.1 vs Veo 3.1 Fast vs Lite — Which to Call from Your API
- Veo API Without Google Cloud: What You Skip and What You Keep
- Veo 3.1 Image-to-Video API: Request Shape and Workflow
- Veo 3.1 Cost Control: Drafts, Caching and Tier Gating
VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →