Reference
Generate video
Video models work differently from chat completions. Generation is asynchronous: you submit a task, get back a task id, and poll for the result. Currently the only video model is doubao-seedance-2-0-pro (ByteDance Seedance 2.0 Pro on Volcengine Ark).
1. Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | https://chinzy.com/v1/videos/tasks | Submit a generation task. Returns { "id": "cgt-..." } immediately. |
GET | https://chinzy.com/v1/videos/tasks/{taskId} | Poll task status. Returns the upstream payload verbatim. |
Both endpoints take the same Authorization: Bearer tsk_... header you use with chat completions. See Use your API key if you don't have a key yet.
queued → running → succeeded (or failed, cancelled, expired). Typical render time is 60–180 seconds for a 4-second 720p clip.2. Submit a task
curl
curl https://chinzy.com/v1/videos/tasks \
-H "Authorization: Bearer $TOKEN_RELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "doubao-seedance-2-0-pro",
"content": [
{"type": "text", "text": "a calm sunset over rolling hills --rs 720p --dur 4"}
],
"ratio": "16:9"
}'Request fields
| Field | Type | Notes |
|---|---|---|
model | string | Required. Use doubao-seedance-2-0-pro. (Other video aliases will appear here as they ship.) |
content | array | Required. Each item is a typed block: text, image_url, video_url, or audio_url. Inline base64 data URLs are accepted. |
ratio | string | Aspect ratio: 16:9, 9:16, 4:3, 3:4, 21:9, 1:1, or adaptive. |
resolution | string | 480p, 720p, 1080p, or 2K. You can also pass it inline in the prompt as --rs 1080p. |
duration | number | 4–15 seconds. Inline form: --dur 5. |
generate_audio | boolean | Whether to render an audio track alongside the video. |
seed | number | Optional reproducibility seed. |
3. Poll until done
# poll-loop.sh
TASK_ID="cgt-..."
while true; do
RESP=$(curl -s "https://chinzy.com/v1/videos/tasks/$TASK_ID" \
-H "Authorization: Bearer $TOKEN_RELAY_KEY")
STATUS=$(echo "$RESP" | jq -r .status)
echo "[\$(date +%T)] $STATUS"
case "$STATUS" in
succeeded)
echo "$RESP" | jq -r .content.video_url
break
;;
failed|cancelled|expired)
echo "$RESP" | jq -r .error
exit 1
;;
esac
sleep 10
donePython
import os, time, requests
BASE = "https://chinzy.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TOKEN_RELAY_KEY']}"}
def submit(prompt: str, *, ratio="16:9", resolution="720p", duration=4):
r = requests.post(f"{BASE}/videos/tasks", headers=HEADERS, json={
"model": "doubao-seedance-2-0-pro",
"content": [{"type": "text", "text": prompt}],
"ratio": ratio, "resolution": resolution, "duration": duration,
})
r.raise_for_status()
return r.json()["id"]
def wait(task_id: str, *, timeout=600, interval=10):
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/videos/tasks/{task_id}", headers=HEADERS)
r.raise_for_status()
body = r.json()
if body["status"] == "succeeded":
return body["content"]["video_url"]
if body["status"] in ("failed", "cancelled", "expired"):
raise RuntimeError(body.get("error") or body["status"])
time.sleep(interval)
raise TimeoutError(task_id)
task_id = submit("a calm sunset over rolling hills")
url = wait(task_id)
print(url)Node.js
const BASE = 'https://chinzy.com/v1';
const headers = { Authorization: `Bearer ${process.env.TOKEN_RELAY_KEY}` };
async function submit(prompt) {
const r = await fetch(`${BASE}/videos/tasks`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'doubao-seedance-2-0-pro',
content: [{ type: 'text', text: prompt }],
ratio: '16:9',
resolution: '720p',
duration: 4,
}),
});
if (!r.ok) throw new Error(await r.text());
return (await r.json()).id;
}
async function wait(taskId, { interval = 10_000, timeoutMs = 600_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const r = await fetch(`${BASE}/videos/tasks/${taskId}`, { headers });
if (!r.ok) throw new Error(await r.text());
const body = await r.json();
if (body.status === 'succeeded') return body.content.video_url;
if (['failed', 'cancelled', 'expired'].includes(body.status)) {
throw new Error(body.error?.message ?? body.status);
}
await new Promise((res) => setTimeout(res, interval));
}
throw new Error('timeout');
}
const taskId = await submit('a calm sunset over rolling hills');
console.log(await wait(taskId));4. The video URL
On succeeded the response includes content.video_url — a direct MP4 link to ByteDance object storage. Two things to know about it:
- It expires after 24 hours. Download or re-host the file inside that window. Don't store the signed URL itself long-term.
- It bypasses the relay. The download is direct client-to-Volcengine; we never see those bytes, so no relay bandwidth or wallet charge applies to fetching the result.
5. Pricing & metering
Video is billed by tokens, the same way the chat models are. The token count is reported in the usage.total_tokens field of the succeeded response. As a rough guide, a 4-second 720p clip is ≈ 87,000 tokens (~$0.08 at the current resale rate). 1080p and longer clips scale roughly linearly.
Settlement happens at the moment of the first poll that observes succeeded. Subsequent polls are free — the relay idempotently skips re-charging a task it has already settled.
6. Ownership & access
A task is bound to the API key that created it. The video URL is only retrievable by the same key, for 7 days. After 7 days the task itself falls out of the upstream history and the relay returns 404; download anything you need before then.
7. Common errors
| Status | Meaning | Fix |
|---|---|---|
400 | Body missing model or content, or one of them is malformed. | Confirm content is a non-empty array of typed blocks. |
403 (on GET) | You're polling a task created by a different API key. | Use the same key that created the task. If you rotated keys, re-submit the task. |
404 | Task id is unknown to the relay (typo, expired after 7 days, or created against a different deployment). | Resubmit; verify the id matches the one returned at create. |
503 | The Volcengine Ark backend is unavailable, or DOUBAO_API_KEY isn't configured on this deployment. | Retry with backoff; if persistent, check your status page. |
Found something missing or wrong in this doc? Open an issue or ping an admin.