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.
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:
| Variable | Purpose |
|---|---|
VIZO_API_KEY | Project key with models:read, generations:create, generations:read, generations:list (to find a task after a lost response) and assets:download. |
VIZO_BILLING_KEY | Read-only billing key with billing:read, used only to check the charge at the end. |
Get an API key
GET/v1/modelsCreate 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
listModelsto confirm the key works. Each entry is one route your project can order, with itsmodelId,operation,inputSchemaand 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": "" }Submit the generation
POST/v1/generationsSend
model,operationandinputstraight toPOST /v1/generations. HTTP 202 means the task is accepted; save the returnedidat once.A new task may be
pending_fundingfor a moment: no Credits are reserved until funding is confirmed, then it becomesqueued. If funding is refused, the task closes witherror.codeset toinsufficient_balance,budget_exceededorspending_blocked.Set
metadata.client_referenceto 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 withlistGenerationsand find it bymetadata.client_reference. The list is newest first. Narrow it withmodel,source,status,created_after(inclusive) andcreated_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 nowNode.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 nowResponse202{ "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" } }Wait for the result
GET/v1/generations/{id}Poll the same task until
closed_atis not null. A task has three separate states; read each one:execution_status: progress of the model rundelivery_status: whether the result is stored;readymeans you can download itbilling_status: held, charged or released
When
closed_atis set andstatusis notsucceeded, there is no result to download. Readerror.codeto see why, for exampleinsufficient_balance,budget_exceededorspending_blocked. A task that never received funding haschargedof0.000.Prefer webhooks to polling? Create an endpoint in the console and pass
webhook_endpoint_idwhen 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" } }Download the result
GET/v1/assets/{id}/downloadUse
result.asset_idto 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 withdeleteAsset.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" }Check the charge
GET/v1/ledgerRead
/v1/ledgerwith a read-only billing key and follownext_cursorthrough every page. Subtract the reversals from the settlement entries of this task; the result equals the task'sprice.charged.A failed or timed-out task shows
chargedas0.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: breakNode.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.