Skip to content
Docs version r15
OpenAPIJSONYAML
Guides / Create your first video

Create your first video

Run one complete call on your own server: submit, wait, download, then check the charge. The example sends a five-second Seedance 2.0 Fast request.

Markdown fileTry in PlaygroundDocs version r15

Before you start

You need a verified account with Credits, and a project that can use the model. Set two environment variables on your server:

VariablePurpose
VIZO_API_KEYProject key with models:read, generations:create, generations:read, generations:list (to find a task after a lost response) and assets:download.
VIZO_BILLING_KEYRead-only billing key with billing:read, used only to check the charge at the end.
  1. Get an API key

    GET/v1/models

    Create a project key in the console under API keys. The full key is shown once, so store it in your server secrets right away. Never put it in a web page, an app or a repository.

    Call listModels to confirm the key works. Each entry is one route your project can order, with its modelId, operation, inputSchema and reference price.

    cURL
    # Keep the key on the server only
    export VIZO_API_KEY="va_xxxxxxxx"
    
    curl https://api.vizoapi.com/v1/models \
      -H "Authorization: Bearer $VIZO_API_KEY"
    Python
    import os, requests
    
    api = "https://api.vizoapi.com/v1"
    headers = {"Authorization": f"Bearer {os.environ['VIZO_API_KEY']}"}
    
    models = requests.get(f"{api}/models", headers=headers, timeout=30).json()
    for m in models["entries"]:
        print(m["modelId"], m["operation"])
    Node.js
    const api = "https://api.vizoapi.com/v1";
    const headers = {
      Authorization: `Bearer ${process.env.VIZO_API_KEY}`,
      "Content-Type": "application/json",
    };
    
    const models = await fetch(`${api}/models`, { headers }).then((r) => r.json());
    console.log(models.entries.map((m) => `${m.modelId} ${m.operation}`));
    Response200
    {
      "entries": [
        {
          "modelId": "seedance-2-0-fast",
          "operation": "text-to-video",
          "routeId": "seedance-2-0-fast-720p",
          "inputSchema": {
            "…": "…"
          }
        }
      ],
      "next_cursor": ""
    }
  2. Submit the generation

    POST/v1/generations

    Send model, operation and input straight to POST /v1/generations. HTTP 202 means the task is accepted; save the returned id at once.

    A new task may be pending_funding for a moment: no Credits are reserved until funding is confirmed, then it becomes queued. If funding is refused, the task closes with error.code set to insufficient_balance, budget_exceeded or spending_blocked.

    Set metadata.client_reference to your own order number. It lets you find the task again if the response is lost.

    Send the create request once. Every accepted POST is its own task with its own hold and charge, even when the content is identical. Turn off automatic retries for this request in your HTTP client and proxy.

    After a timeout or disconnect, look before you resubmit. With a task ID, read the task with getGeneration. Without one, list recent tasks with listGenerations and find it by metadata.client_reference. The list is newest first. Narrow it with model, source, status, created_after (inclusive) and created_before (exclusive).

    cURL
    # Send once: do not let a client or proxy retry this request
    curl https://api.vizoapi.com/v1/generations \
      -H "Authorization: Bearer $VIZO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "seedance-2-0-fast",
        "operation": "text-to-video",
        "input": { "prompt": "A paper boat drifting on a pond", "duration_seconds": 5 },
        "metadata": { "client_reference": "order-1042" }
      }'
    Python
    import os, requests
    
    api = "https://api.vizoapi.com/v1"
    headers = {"Authorization": f"Bearer {os.environ['VIZO_API_KEY']}"}
    
    # Send once: do not retry this request automatically
    task = requests.post(f"{api}/generations", headers=headers, json={
        "model": "seedance-2-0-fast",
        "operation": "text-to-video",
        "input": {"prompt": "A paper boat drifting on a pond", "duration_seconds": 5},
        "metadata": {"client_reference": "order-1042"},
    }, timeout=60).json()
    
    task_id = task["id"]  # save it now
    Node.js
    const api = "https://api.vizoapi.com/v1";
    const headers = {
      Authorization: `Bearer ${process.env.VIZO_API_KEY}`,
      "Content-Type": "application/json",
    };
    
    // Send once: do not retry this request automatically
    const task = await fetch(`${api}/generations`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        model: "seedance-2-0-fast",
        operation: "text-to-video",
        input: { prompt: "A paper boat drifting on a pond", duration_seconds: 5 },
        metadata: { client_reference: "order-1042" },
      }),
    }).then((r) => r.json());
    
    const taskId = task.id; // save it now
    Response202
    {
      "id": "gen_01JD8Z5QW3XR",
      "status": "pending_funding",
      "execution_status": "pending_funding",
      "delivery_status": "pending",
      "billing_status": "pending",
      "version": 1,
      "price": {
        "unit": "credit",
        "reserved": "0.000",
        "quoted": "1400.000",
        "charged": "0.000"
      },
      "allowed_actions": [
        "cancel"
      ],
      "customer_resolution_deadline": "2026-11-03T08:00:12Z",
      "created_at": "2026-11-02T08:00:12Z",
      "model": "seedance-2-0-fast",
      "source": "api",
      "closed_at": null,
      "late_result_available": false,
      "error": null,
      "result": null,
      "metadata": {
        "client_reference": "order-1042"
      }
    }
  3. Wait for the result

    GET/v1/generations/{id}

    Poll the same task until closed_at is not null. A task has three separate states; read each one:

    • execution_status: progress of the model run
    • delivery_status: whether the result is stored; ready means you can download it
    • billing_status: held, charged or released

    When closed_at is set and status is not succeeded, there is no result to download. Read error.code to see why, for example insufficient_balance, budget_exceeded or spending_blocked. A task that never received funding has charged of 0.000.

    Prefer webhooks to polling? Create an endpoint in the console and pass webhook_endpoint_id when you submit. After verifying the signature, read the task by ID and trust that read.

    cURL
    curl https://api.vizoapi.com/v1/generations/$GENERATION_ID \
      -H "Authorization: Bearer $VIZO_API_KEY"
    Python
    import os, requests
    
    api = "https://api.vizoapi.com/v1"
    headers = {"Authorization": f"Bearer {os.environ['VIZO_API_KEY']}"}
    import time
    
    while True:
        task = requests.get(f"{api}/generations/{task_id}", headers=headers, timeout=30).json()
        if task["closed_at"]:
            break
        time.sleep(5)
    
    if task["status"] != "succeeded":
        # Closed without a result, for example insufficient_balance, budget_exceeded or spending_blocked
        raise SystemExit(f"task closed: {task['status']} {(task['error'] or {}).get('code')}")
    print(task["status"], task["price"]["charged"])
    Node.js
    const api = "https://api.vizoapi.com/v1";
    const headers = {
      Authorization: `Bearer ${process.env.VIZO_API_KEY}`,
      "Content-Type": "application/json",
    };
    
    let task;
    do {
      await new Promise((r) => setTimeout(r, 5000));
      task = await fetch(`${api}/generations/${taskId}`, { headers }).then((r) => r.json());
    } while (!task.closed_at);
    
    if (task.status !== "succeeded") {
      // Closed without a result, for example insufficient_balance, budget_exceeded or spending_blocked
      throw new Error(`task closed: ${task.status} ${task.error?.code}`);
    }
    console.log(task.status, task.price.charged);
    Response200
    {
      "id": "gen_01JD8Z5QW3XR",
      "status": "succeeded",
      "execution_status": "succeeded",
      "delivery_status": "ready",
      "billing_status": "settled",
      "version": 4,
      "price": {
        "unit": "credit",
        "reserved": "1400.000",
        "quoted": "1400.000",
        "charged": "1400.000"
      },
      "allowed_actions": [],
      "customer_resolution_deadline": "2026-11-03T08:00:12Z",
      "created_at": "2026-11-02T08:00:12Z",
      "model": "seedance-2-0-fast",
      "source": "api",
      "closed_at": "2026-11-02T08:01:43Z",
      "late_result_available": false,
      "error": null,
      "result": {
        "asset_id": "ast_01JD8Z9M2C4V",
        "outputs": [
          {
            "index": 0,
            "asset_id": "ast_01JD8Z9M2C4V",
            "content_type": "video/mp4"
          }
        ]
      },
      "metadata": {
        "client_reference": "order-1042"
      }
    }
  4. Download the result

    GET/v1/assets/{id}/download

    Use result.asset_id to request a short-lived download URL, then fetch that URL on its own. Do not send the API key with it. When the URL expires, request a new one for the same asset.

    When a task has several outputs, download them in the order of result.outputs. Finished files are kept for 7 days; remove one earlier with deleteAsset.

    cURL
    # 1. Ask for a short-lived download URL
    curl https://api.vizoapi.com/v1/assets/$ASSET_ID/download \
      -H "Authorization: Bearer $VIZO_API_KEY"
    
    # 2. Fetch the returned url on its own, without the API key
    curl -o result.mp4 "$DOWNLOAD_URL"
    Python
    import os, requests
    
    api = "https://api.vizoapi.com/v1"
    headers = {"Authorization": f"Bearer {os.environ['VIZO_API_KEY']}"}
    
    if task["status"] != "succeeded" or not task["result"]:
        raise SystemExit("no result to download: read task['error']['code']")
    asset_id = task["result"]["asset_id"]
    dl = requests.get(f"{api}/assets/{asset_id}/download", headers=headers, timeout=30).json()
    
    # Fetch the signed URL on its own, without the API key
    with requests.get(dl["url"], stream=True, timeout=300) as r, open("result.mp4", "wb") as f:
        for chunk in r.iter_content(1 << 20):
            f.write(chunk)
    Node.js
    import { writeFile } from "node:fs/promises";
    
    if (task.status !== "succeeded" || !task.result) throw new Error("no result to download: read task.error.code");
    const dl = await fetch(`${api}/assets/${task.result.asset_id}/download`, { headers }).then((r) => r.json());
    
    // Fetch the signed URL on its own, without the API key
    const media = await fetch(dl.url);
    await writeFile("result.mp4", Buffer.from(await media.arrayBuffer()));
    Response200
    {
      "url": "https://media.vizoapi.com/d/ast_01JD8Z9M2C4V?sig=…",
      "url_expires_at": "2026-11-02T08:16:43Z"
    }
  5. Check the charge

    GET/v1/ledger

    Read /v1/ledger with a read-only billing key and follow next_cursor through every page. Subtract the reversals from the settlement entries of this task; the result equals the task's price.charged.

    A failed or timed-out task shows charged as 0.000, and its hold has returned to your available balance.

    cURL
    curl "https://api.vizoapi.com/v1/ledger?limit=100" \
      -H "Authorization: Bearer $VIZO_BILLING_KEY"
    
    # When next_cursor is not empty, read the next page
    curl "https://api.vizoapi.com/v1/ledger?limit=100&cursor=$NEXT_CURSOR" \
      -H "Authorization: Bearer $VIZO_BILLING_KEY"
    Python
    import os, requests
    
    api = "https://api.vizoapi.com/v1"
    billing = {"Authorization": f"Bearer {os.environ['VIZO_BILLING_KEY']}"}
    
    entries, cursor = [], ""
    while True:
        page = requests.get(f"{api}/ledger", headers=billing, timeout=30,
                            params={"limit": 100, "cursor": cursor}).json()
        entries += page["items"] or []
        cursor = page.get("next_cursor")
        if not cursor:
            break
    Node.js
    const api = "https://api.vizoapi.com/v1";
    const billing = { Authorization: `Bearer ${process.env.VIZO_BILLING_KEY}` };
    
    const entries = [];
    let cursor = "";
    do {
      const page = await fetch(`${api}/ledger?limit=100&cursor=${cursor}`, { headers: billing }).then((r) => r.json());
      entries.push(...(page.items ?? []));
      cursor = page.next_cursor;
    } while (cursor);
    Response200
    {
      "items": [
        {
          "id": "le_01JD8ZA1T6",
          "reference": "gen_01JD8Z5QW3XR",
          "from": "wallet:held",
          "to": "platform:settled",
          "credits": "1400.000",
          "created_at": "2026-11-02T08:01:43Z"
        }
      ],
      "next_cursor": ""
    }

Amounts and IDs in sample responses are examples. Your requests return their own.

Next