Video Generation and Material Library API
Create video tasks, manage reference materials, and retrieve generated results using the Volcano Engine-compatible format.
This guide is for developers integrating video generation and human-reference material capabilities into a business application. The API uses the Volcano Engine-compatible format. Replace the Base URL, credentials, and model name for your account.
API information
| Item | Value |
|---|---|
| Production Base URL | https://ai.tos.run |
| Video format | Volcano Engine-compatible format |
| Input types | Text, image, video, and audio |
| Authentication | API Key |
Recommended integration flow
- Create an API Key in the console and confirm that it has video access.
- For one-off generation, send a publicly reachable image or video URL directly in the video request.
- For reusable materials, create a material group, create an asset, and save the returned
asset-...ID. - Poll the asset until its status is
Active. - Create a video task and reference the asset with
asset://asset-.... - Save the task ID and poll until
succeededorfailed. - On success, download the video from
content.video_urlpromptly.
Human validation is optional. Use those endpoints only when your account has the corresponding capability enabled.
Authentication
API Key authentication (recommended)
Keep the API Key on your server. Never put it in browser code, a mobile application bundle, or a public repository.
export TOS_API_KEY="gk_YOUR_KEY"Send these headers with every request:
Authorization: Bearer gk_YOUR_KEY
Content-Type: application/jsonVideo and material endpoints require video access. A missing or unauthorized key returns 401 or 403.
Endpoints
Video endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v3/contents/generations/tasks | Create a video task |
GET | /api/v3/contents/generations/tasks/{task_id} | Get a video task |
Material endpoints
All material operations use:
POST /api/material?Action={Action}&Version=2024-01-01| Action | Description |
|---|---|
CreateAssetGroup | Create a material group |
GetAssetGroup | Get a material group |
ListAssetGroups | List material groups |
UpdateAssetGroup | Update a group name or description |
DeleteAssetGroup | Delete a material group |
CreateAsset | Create a material asset |
GetAsset | Get an asset |
ListAssets | List assets |
UpdateAsset | Update an asset name |
DeleteAsset | Delete an asset |
CreateVisualValidateSession | Create a human-validation session |
GetVisualValidateResult | Get a human-validation result |
CreateRealValidateH5 | Create a human-validation management page |
ListAssetGroup and ListAsset are compatibility aliases. New integrations should use ListAssetGroups and ListAssets.
Material responses use the Volcano-compatible envelope:
{
"ResponseMetadata": {
"RequestId": "2026062317822087486485654725234569",
"Action": "CreateAssetGroup",
"Version": "2024-01-01",
"Service": "ark",
"Region": "cn-beijing"
},
"Result": {}
}Always save ResponseMetadata.RequestId for troubleshooting. A material business error may be returned with HTTP 200, so also inspect Result.Error.
Material permissions and states
Material APIs require the capability to be enabled for the account. The service checks account ownership, review status, and channel availability before allowing an asset to be used.
| Status | Description |
|---|---|
Processing | The asset is being processed or reviewed |
Active | The asset can be used for video generation |
Failed | Processing or review failed; inspect Error |
Creating an asset returns its ID immediately, but the asset may not be ready. Poll GetAsset or ListAssets until the status is Active.
Material group API
export TOS_API_KEY="gk_YOUR_KEY"
export BASE_URL="https://ai.tos.run"Create a material group
| Field | Type | Required | Description |
|---|---|---|---|
Name | string | Yes | Group name |
Description | string | No | Group description |
model / Model | string | No | Material model; selected from account permissions when omitted |
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=CreateAssetGroup&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"Name": "product-references",
"Description": "product reference assets"
}'Save Result.Id, for example group-20260623180314-r2rc7.
Get a material group
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=GetAssetGroup&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"Id":"group-20260623180314-r2rc7"}'Common fields include Id, Name, Description, GroupType, ProjectName, Status, CreateTime, and UpdateTime.
List material groups
| Field | Type | Default | Description |
|---|---|---|---|
PageNumber | integer | 1 | Page number, starting at 1 |
PageSize | integer | 10 | Number of items, maximum 100 |
Filter.GroupType | string | - | Filter by group type |
model / Model | string | - | Filter by model |
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=ListAssetGroups&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"PageNumber": 1,
"PageSize": 10,
"Filter": {"GroupType": "AIGC"}
}'The list is in Result.Items, with TotalCount, PageNumber, and PageSize.
Update a material group
UpdateAssetGroup updates the name or description. Pass an empty string explicitly to clear the description.
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=UpdateAssetGroup&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"Id": "group-20260623180314-r2rc7",
"Name": "updated-product-references",
"Description": "updated description"
}'Delete a material group
Confirm that no business workflow still references the group before deleting it. Deleting a group also removes its material records.
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=DeleteAssetGroup&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"Id":"group-20260623180314-r2rc7"}'Material asset API
Create an asset
The source URL must be publicly reachable by the video service. It cannot require login cookies or access to a private network.
| Field | Type | Required | Description |
|---|---|---|---|
GroupId | string | Yes | Material group ID |
Name | string | No | Asset name |
AssetType | string | Yes | Image, Video, or Audio |
URL | string | Yes | Public material URL |
model / Model | string | No | Material model; usually inferred from the group |
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=CreateAsset&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"GroupId": "group-20260623180314-r2rc7",
"Name": "person-reference",
"AssetType": "Image",
"URL": "YOUR_PUBLIC_IMAGE_URL"
}'Save Result.Id, for example asset-20260623180317-8sxkd. The initial status may be Processing.
Get an asset
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=GetAsset&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"Id":"asset-20260623180317-8sxkd"}'Common fields include Id, GroupId, Name, AssetType, URL, Status, Moderation, Error, ProjectName, CreateTime, and UpdateTime.
List assets
| Field | Type | Default | Description |
|---|---|---|---|
PageNumber | integer | 1 | Page number, starting at 1 |
PageSize | integer | 10 | Number of items, maximum 100 |
GroupId | string | - | Filter by one group |
AssetType | string | Image | Image, Video, or Audio |
Name / Keyword | string | - | Fuzzy search by name or asset ID |
Filter.GroupId | string | - | Filter by one group |
Filter.GroupIds | string[] | - | Filter by group IDs |
Filter.AssetType | string | Image | Filter by asset type |
Filter.Statuses | string[] | - | Processing, Active, or Failed |
Filter.Name / Filter.Keyword | string | - | Fuzzy search by name or asset ID |
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=ListAssets&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"PageNumber": 1,
"PageSize": 10,
"Filter": {
"GroupIds": ["group-20260623180314-r2rc7"],
"AssetType": "Image",
"Statuses": ["Active"]
}
}'The list is in Result.Items, with TotalCount, PageNumber, and PageSize.
Update an asset
UpdateAsset updates the asset name:
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=UpdateAsset&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"Id": "asset-20260623180317-8sxkd",
"Name": "updated-person-reference"
}'Delete an asset
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=DeleteAsset&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"Id":"asset-20260623180317-8sxkd"}'Reference an asset in a video request with an asset:// URI, not with the raw asset ID:
{
"type": "image_url",
"image_url": {"url": "asset://asset-20260623180317-8sxkd"},
"role": "reference_image"
}Human-validation API
These endpoints require the corresponding capability to be enabled for the account. Session tokens and H5 links may expire; save them only for the duration required by the workflow.
Create a validation session
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=CreateVisualValidateSession&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'The result may contain BytedToken, H5Link, QrCode, and ExpiresIn.
Get the validation result
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=GetVisualValidateResult&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"BytedToken":"validate-token-xxxx"}'After successful validation, Result.GroupId is the material group that can be used by the account.
Create a validation management page
curl --noproxy '*' -X POST "$BASE_URL/api/material?Action=CreateRealValidateH5&Version=2024-01-01" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'The result normally contains Result.H5Link and Result.ExpiresIn.
Video generation API
Create a video task
POST /api/v3/contents/generations/tasks| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Authorized video model |
content | array | Yes | Text, image, video, and audio inputs |
callback_url | string | No | Task status callback URL |
return_last_frame | boolean | No | Request content.last_frame_url |
service_tier | string | No | Service tier |
execution_expires_after | integer | No | Task execution lifetime in seconds |
generate_audio | boolean | No | Generate or preserve output audio |
draft | boolean | No | Draft or preview mode |
resolution | string | No | For example 480p, 720p, or 1080p |
ratio | string | No | For example 16:9, 9:16, or 1:1 |
duration | integer | No | Duration in seconds |
frames | integer | No | Desired total frame count |
seed | integer | No | Random seed |
camera_fixed | boolean | No | Try to keep the camera fixed |
watermark | boolean | No | Add a watermark |
tools | object[] | No | Model tool configuration |
safety_identifier | string | No | Business trace identifier; never put secrets here |
curl --noproxy '*' -X POST "$BASE_URL/api/v3/contents/generations/tasks" \
-H "Authorization: Bearer $TOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "doubao-seedance-2-0-260128",
"content": [
{"type":"text","text":"A natural, cinematic running shot with stable camera; no subtitles or watermark"},
{
"type":"image_url",
"image_url":{"url":"asset://asset-20260623180317-8sxkd"},
"role":"reference_image"
}
],
"resolution":"480p",
"ratio":"16:9",
"duration":4,
"watermark":false
}'Create response:
{
"id": "cgt-20260731120000-example",
"status": "queued",
"model": "doubao-seedance-2-0-260128",
"created_at": 1782208997
}Save id and use it as task_id when polling.
Content items
| Item | Description |
|---|---|
type=text | text contains the video prompt |
type=image_url | A public image URL or asset://asset-... |
type=video_url | A publicly reachable reference video URL or an available material reference |
type=audio_url | A publicly reachable WAV or MP3 URL |
role | Image: reference_image, first_frame, or last_frame; video: reference_video; audio: reference_audio |
Example with first frame, last frame, and a reference video:
[
{"type":"text","text":"Have the person run from the left side of the frame to the right"},
{"type":"image_url","role":"first_frame","image_url":{"url":"YOUR_PUBLIC_FIRST_FRAME_URL"}},
{"type":"image_url","role":"last_frame","image_url":{"url":"YOUR_PUBLIC_LAST_FRAME_URL"}},
{"type":"video_url","role":"reference_video","video_url":{"url":"YOUR_PUBLIC_REFERENCE_VIDEO_URL"}}
]Use reference audio
Reference audio must use type=audio_url and role=reference_audio. The same request must also include at least one image or video; an audio-only request is invalid.
{
"model": "doubao-seedance-2-0-260128",
"content": [
{"type":"image_url","role":"reference_image","image_url":{"url":"asset://asset-xxxx"}},
{"type":"audio_url","role":"reference_audio","audio_url":{"url":"YOUR_PUBLIC_REFERENCE_AUDIO_URL"}}
],
"resolution":"480p",
"ratio":"16:9",
"duration":4,
"generate_audio":true
}| Item | Description |
|---|---|
| Formats | WAV and MP3 |
| Maximum | Three audio references per request |
| Combination | At least one image or video is required |
| URL | Must be directly downloadable by the video service |
| Recommendation | Standard MP3, such as 44.1 kHz and 128 kbps |
audio_url supplies an existing audio input. generate_audio controls whether the output video contains generated or preserved audio.
Get a video task
GET /api/v3/contents/generations/tasks/{task_id}curl --noproxy '*' "$BASE_URL/api/v3/contents/generations/tasks/cgt-20260731120000-example" \
-H "Authorization: Bearer $TOS_API_KEY"| Status | Description |
|---|---|
queued | Queued |
running | Processing |
succeeded | Completed successfully |
failed | Failed |
Successful response:
{
"id": "cgt-20260731120000-example",
"status": "succeeded",
"model": "doubao-seedance-2-0-260128",
"content": {
"video_url": "GENERATED_VIDEO_URL",
"last_frame_url": "GENERATED_LAST_FRAME_URL"
},
"usage": {"completion_tokens": 100858},
"resolution": "480p",
"ratio": "16:9",
"duration": 4
}Common fields include id, status, model, created_at, updated_at, content.video_url, content.last_frame_url, usage, seed, resolution, ratio, duration, frames, framespersecond, generate_audio, draft, tools, safety_identifier, and error. Optional fields depend on the selected model.
The content.video_url may expire. Download it promptly:
curl --noproxy '*' -L "$VIDEO_URL" -o output.mp4Errors and retrying
Material business errors may return HTTP 200 and appear in Result.Error:
{
"ResponseMetadata": {
"RequestId": "2026062317822087486485654725234569",
"Action": "CreateAssetGroup",
"Version": "2024-01-01",
"Service": "ark",
"Region": "cn-beijing"
},
"Result": {
"Error": {"Code":"InvalidAction","Message":"unsupported Action"}
}
}Video errors use this shape:
{
"error": {
"code": "missing_model",
"message": "model is required"
}
}Common HTTP statuses:
| Status | Scenario |
|---|---|
200 | A material business error may still be returned; inspect Result.Error |
400 | Invalid JSON, Action, version, or field |
401 | Missing or invalid API Key |
403 | Missing video, model, or material permission |
404 | Task or material not found |
502 | Upstream service failed or returned an unreadable response |
503 | No video service is temporarily available |
Record the RequestId, Action, HTTP status, and response body when troubleshooting. Retry polling with the original task ID; do not create a new task for every poll. If you configure callback_url, keep polling as a fallback for missed callbacks.
Python example
This example creates a group and asset, waits for the asset to become available, creates a video task, and polls for the result. It requires Python 3.9+ and requests.
pip install requests
export TOS_API_KEY="gk_YOUR_KEY"
export PERSON_IMAGE_URL="YOUR_PUBLIC_IMAGE_URL"import os
import time
import requests
BASE_URL = os.getenv("TOS_BASE_URL", "https://ai.tos.run")
API_KEY = os.environ["TOS_API_KEY"]
MODEL = os.getenv("TOS_VIDEO_MODEL", "doubao-seedance-2-0-260128")
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def material(action, payload=None):
response = requests.post(
f"{BASE_URL}/api/material",
params={"Action": action, "Version": "2024-01-01"},
headers=HEADERS,
json=payload or {},
timeout=30,
)
response.raise_for_status()
body = response.json()
result = body.get("Result") or {}
error = result.get("Error") or {}
if error.get("Code") or error.get("Message"):
raise RuntimeError(f"{action} failed: {error}")
return result
def create_video(payload):
response = requests.post(
f"{BASE_URL}/api/v3/contents/generations/tasks",
headers=HEADERS,
json=payload,
timeout=30,
)
response.raise_for_status()
return response.json()
def get_video(task_id):
response = requests.get(
f"{BASE_URL}/api/v3/contents/generations/tasks/{task_id}",
headers=HEADERS,
timeout=30,
)
response.raise_for_status()
return response.json()
def wait_for_asset(asset_id, timeout=600):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
asset = material("GetAsset", {"Id": asset_id})
if asset.get("Status") == "Active":
return asset
if asset.get("Status") == "Failed":
raise RuntimeError(asset.get("Error"))
time.sleep(5)
raise TimeoutError(f"asset polling timed out: {asset_id}")
def wait_for_video(task_id, timeout=600):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
task = get_video(task_id)
if task.get("status") == "succeeded":
return task
if task.get("status") == "failed":
raise RuntimeError(task.get("error"))
time.sleep(5)
raise TimeoutError(f"video polling timed out: {task_id}")
def main():
group = material("CreateAssetGroup", {
"Name": "person-references",
"Description": "created by Python example",
})
asset = material("CreateAsset", {
"GroupId": group["Id"],
"Name": "person-reference",
"AssetType": "Image",
"URL": os.environ["PERSON_IMAGE_URL"],
})
wait_for_asset(asset["Id"])
task = create_video({
"model": MODEL,
"content": [
{"type": "text", "text": "A stable running shot in natural light; no subtitles"},
{
"type": "image_url",
"image_url": {"url": f"asset://{asset['Id']}"},
"role": "reference_image",
},
],
"resolution": "480p",
"ratio": "16:9",
"duration": 4,
"watermark": False,
})
completed = wait_for_video(task["id"])
print((completed.get("content") or {}).get("video_url"))
if __name__ == "__main__":
main()For one-off image-to-video generation, skip the material group and asset steps and put the public image URL directly in image_url.url. The same material helper can call GetAssetGroup, ListAssetGroups, ListAssets, UpdateAssetGroup, UpdateAsset, DeleteAsset, and DeleteAssetGroup.
Go example
This Go 1.22+ example uses only the standard library. Set an API Key and a public image URL before running it:
export TOS_API_KEY="gk_YOUR_KEY"
export PERSON_IMAGE_URL="YOUR_PUBLIC_IMAGE_URL"package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const model = "doubao-seedance-2-0-260128"
type Client struct { BaseURL, APIKey string; HTTPClient *http.Client }
type materialEnvelope struct { Result json.RawMessage `json:"Result"` }
type materialError struct { Code string `json:"Code"`; Message string `json:"Message"` }
type idResult struct { ID string `json:"Id"`; Status string `json:"Status"` }
type assetResult struct { Status string `json:"Status"` }
type videoTask struct { ID string `json:"id"`; Status string `json:"status"`; Content *videoContent `json:"content"` }
type videoContent struct { VideoURL string `json:"video_url"` }
func NewClient(baseURL, apiKey string) *Client {
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), APIKey: apiKey, HTTPClient: &http.Client{Timeout: 30 * time.Second}}
}
func (c *Client) doJSON(ctx context.Context, method, endpoint string, payload, result any) error {
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload); if err != nil { return fmt.Errorf("marshal request: %w", err) }
body = bytes.NewReader(data)
}
request, err := http.NewRequestWithContext(ctx, method, endpoint, body)
if err != nil { return fmt.Errorf("create request: %w", err) }
request.Header.Set("Authorization", "Bearer "+c.APIKey)
if payload != nil { request.Header.Set("Content-Type", "application/json") }
response, err := c.HTTPClient.Do(request); if err != nil { return fmt.Errorf("send request: %w", err) }
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 8<<20)); if err != nil { return err }
if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(responseBody))) }
if result == nil || len(bytes.TrimSpace(responseBody)) == 0 { return nil }
return json.Unmarshal(responseBody, result)
}
func (c *Client) Material(ctx context.Context, action string, payload, result any) error {
endpoint, err := url.Parse(c.BaseURL + "/api/material"); if err != nil { return err }
query := endpoint.Query(); query.Set("Action", action); query.Set("Version", "2024-01-01"); endpoint.RawQuery = query.Encode()
if payload == nil { payload = map[string]any{} }
var envelope materialEnvelope
if err := c.doJSON(ctx, http.MethodPost, endpoint.String(), payload, &envelope); err != nil { return err }
var errorResult struct { Error *materialError `json:"Error"` }
if err := json.Unmarshal(envelope.Result, &errorResult); err != nil { return err }
if errorResult.Error != nil && (errorResult.Error.Code != "" || errorResult.Error.Message != "") { return fmt.Errorf("%s failed: %s", action, errorResult.Error.Message) }
if result == nil { return nil }
return json.Unmarshal(envelope.Result, result)
}
func (c *Client) CreateVideoTask(ctx context.Context, payload any) (videoTask, error) { var task videoTask; err := c.doJSON(ctx, http.MethodPost, c.BaseURL+"/api/v3/contents/generations/tasks", payload, &task); return task, err }
func (c *Client) GetVideoTask(ctx context.Context, taskID string) (videoTask, error) { var task videoTask; err := c.doJSON(ctx, http.MethodGet, c.BaseURL+"/api/v3/contents/generations/tasks/"+url.PathEscape(taskID), nil, &task); return task, err }
func waitForAsset(ctx context.Context, c *Client, assetID string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var asset assetResult; if err := c.Material(ctx, "GetAsset", map[string]any{"Id": assetID}, &asset); err != nil { return err }
if asset.Status == "Active" { return nil }; if asset.Status == "Failed" { return fmt.Errorf("asset failed: %s", assetID) }
select { case <-ctx.Done(): return ctx.Err(); case <-time.After(5 * time.Second): }
}
return fmt.Errorf("asset polling timed out: %s", assetID)
}
func waitForVideo(ctx context.Context, c *Client, taskID string, timeout time.Duration) (videoTask, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
task, err := c.GetVideoTask(ctx, taskID); if err != nil { return videoTask{}, err }
if task.Status == "succeeded" { return task, nil }; if task.Status == "failed" { return videoTask{}, fmt.Errorf("video task failed: %s", taskID) }
select { case <-ctx.Done(): return videoTask{}, ctx.Err(); case <-time.After(5 * time.Second): }
}
return videoTask{}, fmt.Errorf("video polling timed out: %s", taskID)
}
func run(ctx context.Context) error {
apiKey, imageURL := strings.TrimSpace(os.Getenv("TOS_API_KEY")), strings.TrimSpace(os.Getenv("PERSON_IMAGE_URL"))
if apiKey == "" || imageURL == "" { return fmt.Errorf("TOS_API_KEY and PERSON_IMAGE_URL are required") }
c := NewClient("https://ai.tos.run", apiKey)
var group idResult; if err := c.Material(ctx, "CreateAssetGroup", map[string]any{"Name": "person-references"}, &group); err != nil { return err }
var asset idResult; if err := c.Material(ctx, "CreateAsset", map[string]any{"GroupId": group.ID, "Name": "person-reference", "AssetType": "Image", "URL": imageURL}, &asset); err != nil { return err }
if err := waitForAsset(ctx, c, asset.ID, 10*time.Minute); err != nil { return err }
task, err := c.CreateVideoTask(ctx, map[string]any{"model": model, "content": []any{
map[string]any{"type": "text", "text": "A stable running shot in natural light; no subtitles"},
map[string]any{"type": "image_url", "image_url": map[string]any{"url": "asset://" + asset.ID}, "role": "reference_image"},
}, "resolution": "480p", "ratio": "16:9", "duration": 4, "watermark": false})
if err != nil { return err }; completed, err := waitForVideo(ctx, c, task.ID, 10*time.Minute); if err != nil { return err }
if completed.Content == nil { return fmt.Errorf("video task succeeded without content") }; fmt.Println(completed.Content.VideoURL); return nil
}
func main() { if err := run(context.Background()); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } }Production checklist
- Use
asset://asset-...only for an asset owned by the current account and ready for use. - Do not treat a successful create request as proof that the asset is ready; wait for
Active. - Make sure public image, video, and audio URLs can be downloaded directly by the video service.
- Store group IDs, asset IDs, validation tokens, and video task IDs for reuse, polling, and cleanup.
- Treat optional response fields as nullable because model capabilities differ.
- Confirm that no workflow still references a group before deleting it.
- Keep the API Key on the server and never log it.
See Authentication and API Keys and Model Discovery and Catalog.