QuickstartMarkdown
Submit a generation
POST /v1/generations answers at once with an id and the status queued. The work runs in the background: poll the id, then download the file. This is the whole flow.
KEY=$(uuidgen)
ID=$(curl -s -X POST https://vidlab.ai/v1/generations \
-H "Authorization: Bearer $VIDLAB_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{
"model": "seedance-2.5",
"input": {
"prompt": "A slow push-in on a perfume bottle on wet black stone",
"resolution": "720p",
"duration": 5,
"aspect_ratio": "9:16"
},
"max_cost_usd": 1.50
}' | jq -r .data.id)
# Poll until it finishes. A failed one is free; to retry, make a new key.
while true; do
STATUS=$(curl -s https://vidlab.ai/v1/generations/$ID \
-H "Authorization: Bearer $VIDLAB_API_KEY" | jq -r .data.status)
echo "$STATUS"
case "$STATUS" in succeeded|failed) break ;; esac
sleep 5
done
curl -sL https://vidlab.ai/v1/generations/$ID/content \
-H "Authorization: Bearer $VIDLAB_API_KEY" -o out.mp4import os, time, uuid, requests
API = "https://vidlab.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VIDLAB_API_KEY']}"}
r = requests.post(
f"{API}/generations",
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={
"model": "seedance-2.5",
"input": {
"prompt": "A slow push-in on a perfume bottle on wet black stone",
"resolution": "720p",
"duration": 5,
"aspect_ratio": "9:16",
},
"max_cost_usd": 1.50,
},
)
r.raise_for_status()
job = r.json()["data"]
# Poll until it finishes. A failed one is free; to retry, make a new key.
while job["status"] in ("queued", "running"):
time.sleep(5)
job = requests.get(f"{API}/generations/{job['id']}", headers=HEADERS).json()["data"]
if job["status"] == "succeeded":
file = requests.get(f"{API}/generations/{job['id']}/content", headers=HEADERS)
open("out.mp4", "wb").write(file.content)
else:
print(job["error"]["code"], job["error"]["message"])import { writeFile } from "node:fs/promises";
const API = "https://vidlab.ai/v1";
const headers = { Authorization: `Bearer ${process.env.VIDLAB_API_KEY}` };
const res = await fetch(`${API}/generations`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
model: "seedance-2.5",
input: {
prompt: "A slow push-in on a perfume bottle on wet black stone",
resolution: "720p",
duration: 5,
aspect_ratio: "9:16",
},
max_cost_usd: 1.5,
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
let { data: job } = await res.json();
// Poll until it finishes. A failed one is free; to retry, make a new key.
while (job.status === "queued" || job.status === "running") {
await new Promise((r) => setTimeout(r, 5000));
job = (await (await fetch(`${API}/generations/${job.id}`, { headers })).json()).data;
}
if (job.status === "succeeded") {
const file = await fetch(`${API}/generations/${job.id}/content`, { headers });
await writeFile("out.mp4", Buffer.from(await file.arrayBuffer()));
} else console.error(job.error.code, job.error.message);The answer is 202 with the queued generation:
{
"data": {
"id": "gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c",
"object": "generation",
"status": "queued",
"model": "seedance-2.5",
"variant": "720p",
"units": 5,
"price_usd": 1.05,
"outputs": [],
"created_at": "2026-11-01T18:04:11.000Z",
"status_url": "/v1/generations/gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c",
"result_url": "/v1/jobs/gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c/result"
}
}| Field | Type | Required | What it is |
|---|---|---|---|
model | string | Yes | A model id from GET /v1/models. |
input | object | Yes | The model's inputs, see Input fields. A field the model does not list is refused, never ignored. |
max_cost_usd | number | No | The most you will pay. A route dearer than this is skipped; if none is cheaper the call answers 422 max_cost_exceeded and nothing is held. Quote the exact request first and pass the quote. |
max_wait_s | number | No | The longest you will wait, in seconds. A route expected to take longer is skipped, and no new route is tried once the time has passed. If no route fits, the call answers 422 max_wait_exceeded and nothing is held. |
route | string | No | auto (default) picks the cheapest healthy route and fails over on its own. route-1, route-2 or route-3 starts from that place in the ranking. |
| Header | Required | What it is |
|---|---|---|
Authorization | Yes | Bearer vidlab_sk_... |
Idempotency-Key | Recommended | 8 to 100 characters of letters, digits and _ : . -; a UUID works. The same key returns the same generation and never holds or charges twice. A failed generation is final: to retry, send a new key. See Rate limits. |
What happens when you submit
- The body is checked against the model's options and priced. A problem answers
400or422at once and nothing is held. - The price is reserved on your balance. Without room for it the call answers
429 api_wallet_exhausted. - The generation is stored as
queuedand the answer is202.
The answer carries x-wallet-remaining-cents (what you can still spend after the reservation) and x-wallet-resets-at. These routes answer the same as POST /v1/generations: POST /v1/video-router/generate and POST /v1/image-router/generate.