← Back to Blog

Video Generation API: Veo 3.1 Through One Endpoint

June 7, 2026
Video Generation API: Veo 3.1 Through One Endpoint

Video Generation API: Veo 3.1 Through One Endpoint

Author: CodeGateway Team Published: 2026-06-06 | Last updated: 2026-09-26 Reading time: 12 minutes

TL;DR: The hard part of video generation was never the prompt — it is the plumbing. Every provider you add means another key, another request shape, another invoice. CodeGateway collapses that into one endpoint, POST /v1/video/generate, with one bearer key and a model string that picks the model. One thing to know up front: the endpoint is synchronous — the HTTP request stays open for roughly two minutes and returns the finished video directly, so set a long read timeout instead of building a polling loop.

Model availability (updated 2026-09-26): the video endpoint currently serves one model, google/veo-3.1-fast. bytedance/seedance-2.0 and minimax/hailuo-2.3-fast were delisted on 2026-09-26 and cannot be called; the reason is in "Model availability today", and their parameter contracts are kept in the appendix as a historical record. For the live list, see the models page.

Table of contents

  1. The endpoint and authentication

  2. Quickstart: your first text-to-video call

  3. Model availability today

  4. Parameters and model selection

  5. Image-to-video

  6. Practical notes and gotchas

  7. Pricing

  8. Appendix: historical contracts for delisted models

  9. Wrap-up

  10. Related reading

If you have built anything with generative video lately, you already know the friction. The model you want for cinematic, audio-aware shots is not the model you want for fast iteration, which is not the model you want for fine camera control. So you end up integrating more than one — and the moment you integrate more than one provider, the work stops being about video and starts being about glue.

Each provider ships its own SDK with its own request shape. Each one issues its own API key that you have to store, rotate, and scope. Each one bills separately, so reconciling spend at the end of the month means three dashboards and three CSVs. None of that work makes your videos any better.

CodeGateway's video generation API removes that tax by putting video models behind a single endpoint. You send one request body, you switch models by changing a single model string, and you authenticate with the same key you already use for every other CodeGateway endpoint. Because authentication follows the familiar OpenAI-style bearer pattern, it drops into tooling you have already written.

A short, honest disclaimer before the code: this is not an OpenAI-shaped API. OpenAI does not have a video endpoint, so there is nothing to be compatible with. What is OpenAI-compatible here is only the _auth style_ — a bearer token in an Authorization header. The request and response shapes are CodeGateway's own.

The endpoint and authentication

There is one endpoint:

POST https://api.codegateway.dev/v1/video/generate

Authentication is a bearer token in the HTTP header — the same CODEGATEWAY_API_KEY you use everywhere else on the platform:

Authorization: Bearer <CODEGATEWAY_API_KEY>

The single most important behavioral fact about this endpoint is that it is synchronous and blocking. There is no job ID, no polling endpoint, and no webhook: the connection stays open while the model renders, and the response body carries the finished video. Measured on production, 2026-09-19: one veo-3.1-fast render came back in 83.4 seconds — a 6-second, 1280x720 MP4, 13.7 MB. Renders routinely run past 100 seconds, and long blocking requests can die in transit: with curl's default HTTP/2 we lost the connection at 109 seconds with curl: (16) Error in the HTTP2 framing layer and zero bytes of body. Pin HTTP/1.1 (--http1.1) or retry in your client, and keep a long read timeout.

The practical consequence: set a long read timeout and pin the HTTP version. A default 30- or 60-second client timeout aborts the request before the video is ready, which reads as "the API failed" when it is simply still rendering. 180 seconds is a sensible floor — every sample below uses it, and the curl sample also forces HTTP/1.1.

A successful call returns HTTP 200 with a JSON body of this shape:

json
{
"created": 1789809317,
"model": "google/veo-3.1-fast",
"serving": "direct-passthrough",
"data": [
{ "url": "https://<object-storage-host>/provider-outputs/<id>.mp4?X-Amz-Expires=86400&X-Amz-Signature=<sig>" }
]
}

The fields you care about:

  • `data[0].url` — the URL of the rendered video. This is what you read, download, and store.

  • `created` — Unix timestamp of when the result was produced.

  • `model` — echoes back the model string you requested.

  • `serving` — a string describing how the file is served (for example, direct-passthrough).

In our 2026-09-19 sample the URL was a presigned object-storage link whose lifetime lives in the URL itself (X-Amz-Expires=86400, i.e. 24 hours) — the response body carried no expires_at field. Treat the link as ephemeral either way: fetch the bytes, write them to your own storage, and serve from there. Do not hotlink data[0].url into a page or store it as a permanent reference.

Bad input returns HTTP 400 with a structured error envelope:

json
{
"error": {
"message": "prompt is required for google/veo-3.1-fast",
"type": "invalid_request_error"
}
}

Read error.message for the human-readable reason and error.type for the error class. You will see this for a missing required prompt, a prompt over the 2000-character cap, a media URL that is not public HTTPS, or a model value that is not in the endpoint's current supported set.

Quickstart: your first text-to-video call

The smallest useful request is a text-to-video call against google/veo-3.1-fast. Below are three copy-paste-runnable versions — curl, Python, and Node/TypeScript — each of which sends a prompt and saves the returned video to disk. Note the long timeout in every one.

curl

bash
curl -sS --http1.1 --max-time 180 \
-X POST https://api.codegateway.dev/v1/video/generate \
-H "Authorization: Bearer $CODEGATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/veo-3.1-fast",
"prompt": "A slow dolly shot through a misty pine forest at dawn, soft volumetric light"
}' \
| tee /tmp/video-response.json

# Pull the URL out of the response and download it
VIDEO_URL=$(python3 -c "import json,sys; print(json.load(open('/tmp/video-response.json'))['data'][0]['url'])")
curl -sS --http1.1 -o veo-output.mp4 "$VIDEO_URL"

Python (requests)

python
import os
import requests

resp = requests.post(
"https://api.codegateway.dev/v1/video/generate",
headers={
"Authorization": f"Bearer {os.environ['CODEGATEWAY_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": "google/veo-3.1-fast",
"prompt": (
"A slow dolly shot through a misty pine forest at dawn, "
"soft volumetric light"
),
},
timeout=180, # the call blocks ~2 min while the video renders
)
resp.raise_for_status()
body = resp.json()
video_url = body["data"][0]["url"]
print("rendered:", video_url)

# Download promptly — the URL is presigned and expires
video = requests.get(video_url, timeout=120)
video.raise_for_status()
with open("veo-output.mp4", "wb") as f:
f.write(video.content)
print("saved veo-output.mp4")

Node / TypeScript (fetch)

typescript
import { writeFile } from "node:fs/promises";

const controller = new AbortController();
// Abort if the render somehow runs past 3 minutes
const timeout = setTimeout(() => controller.abort(), 180_000);

const resp = await fetch("https://api.codegateway.dev/v1/video/generate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODEGATEWAY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/veo-3.1-fast",
prompt:
"A slow dolly shot through a misty pine forest at dawn, soft volumetric light",
}),
signal: controller.signal,
});
clearTimeout(timeout);

if (!resp.ok) {
throw new Error(`video generate failed: ${resp.status} ${await resp.text()}`);
}

const body = await resp.json();
const videoUrl: string = body.data[0].url;
console.log("rendered:", videoUrl);

// Download promptly — the URL is presigned and expires
const file = await fetch(videoUrl);
await writeFile("veo-output.mp4", Buffer.from(await file.arrayBuffer()));
console.log("saved veo-output.mp4");

Three things to carry forward from the quickstart: you select the model with the model string, you read the result from data[0].url, and you set a long timeout because the request blocks. Everything after this is the same pattern with different model-specific fields.

Model availability today

The endpoint's supported set tracks the state of its upstream channels. As of 2026-09-26:

model value

Status

Notes

google/veo-3.1-fast

Available

Text-to-video and image-to-video, with optional generated audio

bytedance/seedance-2.0

Delisted (2026-09-26)

Not callable; parameter contract kept in the appendix

minimax/hailuo-2.3-fast

Delisted (2026-09-26)

Not callable; parameter contract kept in the appendix

When and why they were delisted: both models were removed from the video endpoint and the models page on 2026-09-26. The direct cause is that the upstream video channel they depend on ran out of prepaid credit and could no longer settle charges; leaving them listed would have meant advertising a model that cannot be called. A few earlier variants (google/veo-3.1, minimax/hailuo-2.3, bytedance/seedance-2.0-fast, bytedance/seedance-2.0-mini) were delisted before that for the same class of reason and are handled identically.

What happens now: both models are delisted — those two model values are no longer in the endpoint's supported set, so requests are rejected with 400:

json
{
"error": {
"message": "Unsupported video model: bytedance/seedance-2.0. Supported: google/veo-3.1-fast",
"type": "invalid_request_error"
}
}

(Measured 2026-09-26 with a minimal {"model": "...", "prompt": "..."} body; both delisted ids return the same shape.)

On replacements: Seedance 2.0's advantage was the widest control surface — many aspect ratios, fixed camera, watermark control, reference images and video, first/last frame, and a seed. Those controls currently have no equivalent on the video endpoint. The parameter set for google/veo-3.1-fast is in the next section; do not copy the Seedance parameters from the appendix onto Veo. Whether either model returns is a matter of live state, not of a published plan — check the models page and the changelog.

Parameters and model selection

The available model, google/veo-3.1-fast, handles both text-to-video and image-to-video. The general request shape is:

json
{ "model": "google/veo-3.1-fast", "prompt": "...", "<model-specific field>": "..." }

Field

Type / values

Default

Required

prompt

string, max 2000 characters

—

yes

aspect_ratio

"16:9" / "9:16" / "1:1"

"16:9"

no

duration

string "4s" / "6s" / "8s"

"6s"

no

resolution

"720p" / "1080p"

"720p"

no

generate_audio

boolean

false

no

image_input

base64 string (image-to-video)

—

no

Notes: duration is a string with an s suffix — write "6s", not 6. Audio is off unless you ask for it. Billing is per second and independent of resolution, and the gateway normalizes resolution to the default tier (the measured veo output was 1280x720), so asking for 1080p does not produce a 1080p file.

A request with parameters set:

json
{
"model": "google/veo-3.1-fast",
"prompt": "Neon rain on a quiet Tokyo side street, reflections in puddles",
"aspect_ratio": "9:16",
"duration": "8s",
"resolution": "1080p",
"generate_audio": true
}

Live availability, measured against production on 2026-09-19: google/veo-3.1-fast returned HTTP 200 in 83.4 seconds (a 6-second clip at 1280x720).

Image-to-video

Pass image_input (a base64 string) to google/veo-3.1-fast and the call becomes image-to-video. Here is a runnable example:

python
import base64
import os
import requests

with open("first-frame.jpg", "rb") as f:
frame_b64 = base64.b64encode(f.read()).decode()

resp = requests.post(
"https://api.codegateway.dev/v1/video/generate",
headers={
"Authorization": f"Bearer {os.environ['CODEGATEWAY_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": "google/veo-3.1-fast",
"prompt": "The camera slowly pushes in as the leaves begin to sway in the wind",
"image_input": frame_b64,
"duration": "6s",
},
timeout=180,
)
resp.raise_for_status()
print(resp.json()["data"][0]["url"])

Any URL-based image input must be HTTPS and non-internal — a public https:// URL, not an internal host or a private address. Base64 inline data sidesteps that entirely.

The two delisted models named their image field differently (Seedance used image, Hailuo used first_frame_image), but neither can be called since 2026-09-26; those field names survive only as a historical note in the appendix.

Practical notes and gotchas

A handful of details will save you debugging time:

  • 2000-character prompt cap. Prompts are capped at 2000 characters. Go over and you get a 400. Trim before you send rather than relying on silent truncation.

  • Veo audio is off by default. generate_audio defaults to false. If your clip comes back silent, that is expected — set generate_audio: true to opt in.

  • Handle the synchronous timeout. The request blocks for roughly two minutes. Set a read timeout of at least 180 seconds on the client, and pin HTTP/1.1 if you are calling from curl. Do not build a polling loop or look for a job ID — there is none. If you are calling from a serverless function, make sure its maximum execution time comfortably exceeds the render time.

  • Presigned URL expiry. data[0].url is a time-limited presigned link (24 hours in our measurement; the lifetime lives in the URL's X-Amz-Expires and the response body carries no expires_at). Download the file as soon as you get it and store it yourself. Treat the returned URL as a one-time handle, never a permanent address.

  • HTTPS-only, non-internal media inputs. Any image URL you pass in must be HTTPS and must not point at an internal or private address. If you cannot expose a public HTTPS URL, send the media inline as base64 instead.

  • Use a `model` value from the current supported set. A value outside it returns 400 invalid_request_error, and the error message lists what is supported today.

Pricing

Costs vary by model and by the parameters you choose. For current rates, see the pricing page at https://www.codegateway.dev/pricing.

Appendix: historical contracts for delisted models

This appendix is an archive, not a runnable document. bytedance/seedance-2.0 and minimax/hailuo-2.3-fast were delisted on 2026-09-26 and cannot be called (requests return 400 Unsupported video model). What follows is their pre-delisting parameter reference and request bodies, kept for exactly one purpose: letting code that was written against them line up field semantics while debugging. Do not treat the bodies below as working examples, and do not copy their parameters onto Veo. The current model and its fields are the ones documented above and on the models page.

bytedance/seedance-2.0 (delisted, not callable)

The pre-delisting parameter contract:

Field

Type / values

Default

Required

prompt

string, max 2000 characters

—

yes

aspect_ratio

"16:9" / "4:3" / "1:1" / "3:4" / "9:16" / "21:9" / "9:21"

"16:9"

no

duration

integer, 4–12 seconds

5

no

resolution

"480p" / "720p" / "1080p"

"720p"

no

camera_fixed

boolean

false

no

watermark

boolean

false

no

fps

fixed at 24

24

—

image

string (URL or base64, image-to-video)

—

no

last_frame_image

string

—

no

reference_images

array of strings

—

no

reference_video

string

—

no

seed

integer

—

no

Historical notes: duration was an integer (8, not "8s") — the easiest thing to confuse with Veo. aspect_ratio had the widest set of ratios, and the model also accepted a fixed-camera toggle, watermark control, reference images and video, and first/last frames.

The pre-delisting request body (no longer executable):

json
{
"model": "bytedance/seedance-2.0",
"prompt": "A lone lighthouse on a cliff, storm clouds rolling in, waves crashing",
"aspect_ratio": "21:9",
"duration": 10,
"resolution": "1080p",
"camera_fixed": true,
"watermark": false,
"seed": 42
}

minimax/hailuo-2.3-fast (delisted, not callable)

The pre-delisting parameter contract — note it was image-to-video only, and a text-only request was rejected:

Field

Type / values

Default

Required

prompt

string, max 2000 characters

—

no

duration

enum; examples used 6

—

no

resolution

"768P" / "1080P" (uppercase P)

"768P"

no

first_frame_image

string (image-to-video)

—

no

fast_pretreatment

boolean

false

no

prompt_optimizer

boolean

true

no

Historical notes: resolution used an uppercase P, unlike the lowercase p of the other models. There was no aspect_ratio field. prompt_optimizer was on by default. A first frame (first_frame_image, short edge ≥ 300px) was mandatory, and a text-only request was rejected with a 400.

The pre-delisting request body (no longer executable):

json
{
"model": "minimax/hailuo-2.3-fast",
"first_frame_image": "https://www.codegateway.dev/api/media/file/codegateway-og-default.jpg",
"prompt": "Slow cinematic push-in, gentle light sweep across the card surface",
"duration": 6,
"resolution": "1080P",
"prompt_optimizer": true
}

Wrap-up

Three things to carry into your integration: the endpoint is synchronous, so set a 180-second read timeout and pin HTTP/1.1; google/veo-3.1-fast covers both text-to-video and image-to-video, and its duration is a string like "6s"; and the returned video URL expires (24 hours in our measurement), so download it promptly. Since 2026-09-26 the video endpoint serves that one model — the other two are documented in the appendix as history, and cannot be called.

If you already have a CodeGateway key, you can run the quickstart above as-is — change the prompt and save the result from data[0].url. That is the whole loop.

  • Models page — the live list of video models and every other model's status.

  • Billing guide — top-ups, invoice mechanics, and where to check your balance.

  • Error troubleshooting — working through 400 / 401 / 403 / 429 / 5xx by status code.

  • Changelog — where model additions, delistings, and price changes are recorded.

  • MDN: AbortSignal.timeout() — the standard way to bound a long blocking request in Node or the browser.

  • MDN: fetch() — timeout and AbortSignal semantics for the built-in fetch.

Author:CodeGateway Team

Solve API Key & Billing limits with CodeGateway

Lacking an official API key or facing overseas card payment issues? Create an sk-cg- key instantly. One key for Claude, GPT, Gemini, and DeepSeek: Claude and Gemini text models use the Anthropic Messages protocol (/v1/messages) — the OpenAI-compatible text endpoints do not accept them. Built-in smart routing and free trial credits included.

Sign up free