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

MethodPathPurpose
POSThttps://chinzy.com/v1/videos/tasksSubmit a generation task. Returns { "id": "cgt-..." } immediately.
GEThttps://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.

Task lifecycle. Status moves through queuedrunningsucceeded (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

FieldTypeNotes
modelstringRequired. Use doubao-seedance-2-0-pro. (Other video aliases will appear here as they ship.)
contentarrayRequired. Each item is a typed block: text, image_url, video_url, or audio_url. Inline base64 data URLs are accepted.
ratiostringAspect ratio: 16:9, 9:16, 4:3, 3:4, 21:9, 1:1, or adaptive.
resolutionstring480p, 720p, 1080p, or 2K. You can also pass it inline in the prompt as --rs 1080p.
durationnumber4–15 seconds. Inline form: --dur 5.
generate_audiobooleanWhether to render an audio track alongside the video.
seednumberOptional 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
done

Python

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

StatusMeaningFix
400Body 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.
404Task 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.
503The 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.