Video Generation API: Veo 3.1, Hailuo 2.3 & Seedance 2.0 Through One Endpoint
Author: CodeGateway Team Published: 2026-06-06 Reading time: 12 minutes
TL;DR: A modern video generation API problem is rarely the prompt — it is the plumbing. Three good models live behind three providers, which means three API keys, three request schemas, and three invoices. CodeGateway collapses that into one: a single endpoint, POST /v1/video/generate, with one bearer key, reaches Google Veo 3.1, MiniMax Hailuo 2.3, and ByteDance Seedance 2.0. You pick the model with a string field; everything else stays the same. The catch worth knowing 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.
Table of contents
The problem: three providers, three of everything
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. It is pure integration tax.
CodeGateway's video generation API removes that tax by putting three video models behind a single endpoint:
Google Veo 3.1 (
google/veo-3.1-fast) — Google's Veo 3.1, fast variant, with optional audio.MiniMax Hailuo 2.3 (
minimax/hailuo-2.3-fast) — MiniMax's Hailuo 2.3, fast variant.ByteDance Seedance 2.0 (
bytedance/seedance-2.0) — ByteDance's Seedance 2.0, the richest control surface of the three.
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. One key, one endpoint, three providers — and unified billing as a concept rather than three reconciliations. 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/generateAuthentication 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. You send the request, the connection stays open while the model renders, and the response body contains the finished video URL. In testing, the fast Veo variant returned in roughly 118 to 136 seconds — call it about two minutes. That is normal, not a hang.
The practical consequence: set a long read timeout. A default HTTP client timeout of 30 or 60 seconds will abort the request before the video is ready and you will think the API failed when it is simply still working. A read timeout of 180 seconds is a sensible floor. Every code sample below sets one explicitly.
A successful call returns HTTP 200 with a JSON body of this shape:
{
"created": 1749200000,
"model": "google/veo-3.1-fast",
"serving": "presigned",
"expires_at": 1749203600,
"data": [
{ "url": "https://.../video.mp4?..." }
]
}The fields you care about:
`data[0].url` — the URL of the rendered video. This is what you read, download, and store.
`expires_at` — present when the URL is a presigned, expiring link (Unix seconds). When CodeGateway has storage credentials configured, it returns a presigned GET URL that is valid only for a window; otherwise it returns a raw URL. Either way, treat the link as ephemeral.
`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, presigned versus raw).
The rule that follows from expires_at: download the file promptly and host it yourself. Do not hotlink data[0].url into a web page or store it in a database as a permanent reference — it will stop resolving once it expires. Fetch the bytes, write them to your own storage, and serve from there.
Bad input returns HTTP 400 with a plain error envelope:
{ "error": "prompt is required for google/veo-3.1-fast" }You will see this for things like a missing required prompt, a prompt that exceeds the 2000-character cap (prompt exceeds 2000 chars), or a media input URL that is not HTTPS or points at an internal address.
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
curl -sS --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 -o veo-output.mp4 "$VIDEO_URL"Python (requests)
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, "expires_at:", body.get("expires_at"))
# 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)
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, "expires_at:", body.expires_at);
// 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.
The three models and when to use each
All three models accept text-to-video. Where they differ is the control surface — and, importantly, the exact types those controls expect. The single biggest source of 400 errors when switching models is reusing one model's parameters against another. Veo's duration is the string "6s"; Seedance's duration is the integer 6; Hailuo's resolution is the uppercase "768P" and it has no aspect_ratio field at all. Read the per-model table before you copy a body across.
When to reach for each model:
`google/veo-3.1-fast` — when you want a fast Veo render and, optionally, generated audio. Audio is opt-in, so it is off unless you ask for it. Good default for text-to-video where you may also want a soundtrack.
`bytedance/seedance-2.0` — when you need fine control: a wide range of aspect ratios (including ultra-wide and vertical), integer duration up to 12 seconds, a fixed-camera toggle, watermark control, seeds for reproducibility, and reference inputs. This is the model to choose when "good enough" is not enough and you need to steer the shot.
`minimax/hailuo-2.3-fast` — when you want fast Hailuo output with prompt-side conveniences like an optional prompt optimizer and a fast-pretreatment toggle. Note its resolution values are uppercase (
"768P"/"1080P") and it intentionally has no aspect-ratio control.
Per-model parameter reference
Each row lists the field, its accepted values or type, and the default.
google/veo-3.1-fast
`prompt` — string, required, max 2000 characters.
`aspect_ratio` —
"16:9"|"9:16"|"1:1"(default"16:9").`duration` — string
"4s"|"6s"|"8s"(default"6s"). Note thessuffix; this is a string, not a number.`resolution` —
"720p"|"1080p"(default"720p").`generate_audio` — boolean (default
false; audio is opt-in).`image_input` — base64 string, optional. Supplying it turns the call into image-to-video.
bytedance/seedance-2.0
`prompt` — string, required, max 2000 characters.
`aspect_ratio` —
"16:9"|"4:3"|"1:1"|"3:4"|"9:16"|"21:9"|"9:21"(default"16:9").`duration` — integer, 4 to 12 seconds (default
5). This is a number, not a string.`resolution` —
"480p"|"720p"|"1080p"(default"720p").`camera_fixed` — boolean (default
false).`watermark` — boolean (default
false).`fps` — fixed at 24.
Optional:
generate_audio,image(URL or base64),last_frame_image,reference_images[],reference_video,seed.
minimax/hailuo-2.3-fast
`prompt` — string, optional, max 2000 characters.
`duration` — enum; examples use
6.`resolution` —
"768P"|"1080P"(default"768P"). Note the uppercaseP.`first_frame_image` — optional; supplying it drives image-to-video.
`fast_pretreatment` — boolean (default
false).`prompt_optimizer` — boolean (default
true).No `aspect_ratio` field — do not include one.
Text-to-video request bodies, one per model
google/veo-3.1-fast with audio turned on and a vertical 8-second clip:
{
"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
}bytedance/seedance-2.0 steering an ultra-wide, fixed-camera, 10-second shot with a seed for reproducibility:
{
"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 at 1080P with the prompt optimizer left on:
{
"model": "minimax/hailuo-2.3-fast",
"prompt": "A paper boat drifting down a rain-filled gutter, shallow depth of field",
"duration": 6,
"resolution": "1080P",
"prompt_optimizer": true
}Each of these slots into the exact same request you saw in the quickstart — only the JSON body changes. Send any of them to POST /v1/video/generate with your bearer token and read the result from data[0].url.
Image-to-video
All three models can take an image as a starting frame, but each names the field differently — another place where copying a body across models will earn you a 400.
Veo uses `image_input` (a base64 string).
Seedance uses `image` (a URL or base64 string).
Hailuo uses `first_frame_image`.
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.
Here is a Seedance image-to-video call in Python, animating a still image into a short clip:
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": "bytedance/seedance-2.0",
"prompt": "Gentle wind moves through the wheat field, clouds drift overhead",
"image": "https://example.com/wheat-field.jpg", # must be https + public
"duration": 6,
"resolution": "1080p",
},
timeout=180,
)
resp.raise_for_status()
body = resp.json()
video_url = body["data"][0]["url"]
video = requests.get(video_url, timeout=120)
video.raise_for_status()
with open("seedance-i2v.mp4", "wb") as f:
f.write(video.content)
print("saved seedance-i2v.mp4")To run the same idea on Veo, swap image for image_input and pass a base64 string; on Hailuo, use first_frame_image. The endpoint, auth, timeout, and response handling are identical.
Practical notes and gotchas
A handful of details will save you debugging time:
2000-character prompt cap. Prompts are capped at 2000 characters across all three models. Go over and you get a 400 with
prompt exceeds 2000 chars. Trim before you send rather than relying on silent truncation.Veo audio is off by default.
generate_audiodefaults tofalse. If your Veo clip comes back silent, that is expected — setgenerate_audio: trueto opt in. The other models do not turn audio on for you either.Synchronous timeout handling. The request blocks for roughly two minutes. Set a read timeout of at least 180 seconds on the client. 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, or run the call from a worker that can stay alive.
Presigned URL expiry. When
expires_atis present, thedata[0].urlis a time-limited link. 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 or video 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.
Match parameter types to the model. This is the most common avoidable error: Veo
durationis the string"6s", Seedancedurationis the integer6, and Hailuoresolutionis the uppercase"768P". Hailuo has noaspect_ratiofield — including one is a mistake.
Pricing
Costs vary by model and by the parameters you choose. For current rates, see the pricing page at https://www.codegateway.dev/pricing.
Wrap-up
The hard part of building with generative video was never the prompt — it was reaching three good models without standing up three integrations. CodeGateway's video generation API answers that with one endpoint, one bearer key, and one request shape that covers Google Veo 3.1, MiniMax Hailuo 2.3, and ByteDance Seedance 2.0, for both text-to-video and image-to-video. Switch models by changing a string; respect the per-model parameter types; set a long timeout because the call is synchronous; and download the returned URL promptly because it expires.
If you already have a CodeGateway key, you can run the quickstart above as-is — change the prompt, pick a model, and save the result from data[0].url. That is the whole loop.
Related reading
CodeGateway pricing — current rates for every model.
CodeGateway blog — more API guides and integration walkthroughs.
