# 生成第一个视频

在你自己的服务器上跑通一次完整调用：提交、等待、下载，最后核对费用。示例请求一段五秒的 Seedance 2.0 Fast 视频。

## 开始前

需要一个已验证邮箱、有可用 Credits 的账户，项目已开通要调用的模型。在服务端设置两个环境变量：

| 变量 | 用途 |
| --- | --- |
| `VIZO_API_KEY` | 项目 Key，需要 `models:read`、`generations:create`、`generations:read`、`generations:list`（回包丢失后用来找回任务）和 `assets:download`。 |
| `VIZO_BILLING_KEY` | 只读账单 Key，需要 `billing:read`，只在最后核对费用时使用。 |

## 1. 获取 API Key

`GET /v1/models`

在控制台的 API Keys 创建项目 Key。完整 Key 只显示一次，请立即存进服务端的密钥管理。不要把 Key 放进网页、App 或代码库。

用 `listModels` 确认 Key 可用。返回的每一项是项目可以下单的一个规格，包含 `modelId`、`operation`、`inputSchema` 和参考价。

### cURL

```bash
# 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

```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

```javascript
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}`));
```

### 200

```json
{
  "entries": [
    {
      "modelId": "seedance-2-0-fast",
      "operation": "text-to-video",
      "routeId": "seedance-2-0-fast-720p",
      "inputSchema": {
        "…": "…"
      }
    }
  ],
  "next_cursor": ""
}
```

## 2. 提交生成

`POST /v1/generations`

把 `model`、`operation` 和 `input` 直接发给 `POST /v1/generations`。返回 202 表示任务已受理，请立即保存返回的 `id`。

新任务可能先处于 `pending_funding`：额度确认前不会冻结任何 Credits，确认后变为 `queued`。额度被拒绝时，任务会关闭，`error.code` 为 `insufficient_balance`、`budget_exceeded` 或 `spending_blocked`。

把 `metadata.client_reference` 设为你自己的订单号。回包丢失时，可以用它找回任务。

> **创建请求只发送一次。**每个被受理的 POST 都是独立任务，会分别冻结和扣费，内容完全相同的第二次提交也一样。请关闭 HTTP 客户端和代理对这个请求的自动重试。

> **超时或断连时先查询，再决定是否重新提交。**已经拿到任务 ID，就用 `getGeneration` 查询原任务。没有拿到 ID，就用 `listGenerations` 查看近期任务，按 `metadata.client_reference` 找回它。列表按创建时间从新到旧排列，可用 `model`、`source`、`status`、`created_after`（含）和 `created_before`（不含）缩小范围。

### cURL

```bash
# 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": "池塘里漂着一只纸船", "duration_seconds": 5 },
    "metadata": { "client_reference": "order-1042" }
  }'
```

### Python

```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": "池塘里漂着一只纸船", "duration_seconds": 5},
    "metadata": {"client_reference": "order-1042"},
}, timeout=60).json()

task_id = task["id"]  # save it now
```

### Node.js

```javascript
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: "池塘里漂着一只纸船", duration_seconds: 5 },
    metadata: { client_reference: "order-1042" },
  }),
}).then((r) => r.json());

const taskId = task.id; // save it now
```

### 202

```json
{
  "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. 等待结果

`GET /v1/generations/{id}`

轮询原任务，直到 `closed_at` 不为空。任务有三个独立状态，分别看：

- `execution_status`：模型生成进度
- `delivery_status`：结果是否已保存，`ready` 表示可以下载
- `billing_status`：冻结中、已扣费或已释放

`closed_at` 有值而 `status` 不是 `succeeded` 时，没有可下载的结果。读取 `error.code` 查看原因，例如 `insufficient_balance`、`budget_exceeded` 或 `spending_blocked`。没有拿到额度的任务，`charged` 为 `0.000`。

> 不想轮询？在控制台创建回调端点，提交时传 `webhook_endpoint_id`。收到通知后先验签，再用任务 ID 查询最新状态，以查询结果为准。

### cURL

```bash
curl https://api.vizoapi.com/v1/generations/$GENERATION_ID \
  -H "Authorization: Bearer $VIZO_API_KEY"
```

### Python

```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

```javascript
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);
```

### 200

```json
{
  "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. 下载结果

`GET /v1/assets/{id}/download`

用 `result.asset_id` 申请下载授权，返回一个短期有效的 `url`。再单独请求这个链接拿到文件，不要附带 API Key。链接过期后，对同一个素材重新申请即可。

一次任务有多个结果时，按 `result.outputs` 的顺序逐个下载。成品保存 7 天，可以用 `deleteAsset` 提前删除。

### cURL

```bash
# 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

```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

```javascript
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()));
```

### 200

```json
{
  "url": "https://media.vizoapi.com/d/ast_01JD8Z9M2C4V?sig=…",
  "url_expires_at": "2026-11-02T08:16:43Z"
}
```

## 5. 核对费用

`GET /v1/ledger`

用只读账单 Key 读取 `/v1/ledger`，沿着 `next_cursor` 读完所有分页。把这个任务的结算分录减去冲正分录，结果应等于任务的 `price.charged`。

生成失败或超时的任务，`charged` 为 `0.000`，冻结额度已经退回可用余额。

### cURL

```bash
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

```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

```javascript
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);
```

### 200

```json
{
  "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": ""
}
```

示例响应中的金额和 ID 仅作演示，你的请求会返回各自的值。

## 接下来

- [图片、语音和其他视频模型](/zh/models): 每个模型页列出自己的字段和价格
- [创建生成任务的完整参考](/zh/docs/api/createGeneration): 请求字段、响应和错误码
- [接收 Webhook 通知](/zh/docs/api/generationEvent): 验签、去重和乱序处理
- [核对中断的提交](/zh/docs/retries): 超时、回包丢失和重复任务
