Language
TensorFusion Docs

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

ItemValue
Production Base URLhttps://ai.tos.run
Video formatVolcano Engine-compatible format
Input typesText, image, video, and audio
AuthenticationAPI Key
  1. Create an API Key in the console and confirm that it has video access.
  2. For one-off generation, send a publicly reachable image or video URL directly in the video request.
  3. For reusable materials, create a material group, create an asset, and save the returned asset-... ID.
  4. Poll the asset until its status is Active.
  5. Create a video task and reference the asset with asset://asset-....
  6. Save the task ID and poll until succeeded or failed.
  7. On success, download the video from content.video_url promptly.

Human validation is optional. Use those endpoints only when your account has the corresponding capability enabled.

Authentication

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/json

Video and material endpoints require video access. A missing or unauthorized key returns 401 or 403.

Endpoints

Video endpoints

MethodEndpointDescription
POST/api/v3/contents/generations/tasksCreate 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
ActionDescription
CreateAssetGroupCreate a material group
GetAssetGroupGet a material group
ListAssetGroupsList material groups
UpdateAssetGroupUpdate a group name or description
DeleteAssetGroupDelete a material group
CreateAssetCreate a material asset
GetAssetGet an asset
ListAssetsList assets
UpdateAssetUpdate an asset name
DeleteAssetDelete an asset
CreateVisualValidateSessionCreate a human-validation session
GetVisualValidateResultGet a human-validation result
CreateRealValidateH5Create 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.

StatusDescription
ProcessingThe asset is being processed or reviewed
ActiveThe asset can be used for video generation
FailedProcessing 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

FieldTypeRequiredDescription
NamestringYesGroup name
DescriptionstringNoGroup description
model / ModelstringNoMaterial 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

FieldTypeDefaultDescription
PageNumberinteger1Page number, starting at 1
PageSizeinteger10Number of items, maximum 100
Filter.GroupTypestring-Filter by group type
model / Modelstring-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.

FieldTypeRequiredDescription
GroupIdstringYesMaterial group ID
NamestringNoAsset name
AssetTypestringYesImage, Video, or Audio
URLstringYesPublic material URL
model / ModelstringNoMaterial 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

FieldTypeDefaultDescription
PageNumberinteger1Page number, starting at 1
PageSizeinteger10Number of items, maximum 100
GroupIdstring-Filter by one group
AssetTypestringImageImage, Video, or Audio
Name / Keywordstring-Fuzzy search by name or asset ID
Filter.GroupIdstring-Filter by one group
Filter.GroupIdsstring[]-Filter by group IDs
Filter.AssetTypestringImageFilter by asset type
Filter.Statusesstring[]-Processing, Active, or Failed
Filter.Name / Filter.Keywordstring-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
FieldTypeRequiredDescription
modelstringYesAuthorized video model
contentarrayYesText, image, video, and audio inputs
callback_urlstringNoTask status callback URL
return_last_framebooleanNoRequest content.last_frame_url
service_tierstringNoService tier
execution_expires_afterintegerNoTask execution lifetime in seconds
generate_audiobooleanNoGenerate or preserve output audio
draftbooleanNoDraft or preview mode
resolutionstringNoFor example 480p, 720p, or 1080p
ratiostringNoFor example 16:9, 9:16, or 1:1
durationintegerNoDuration in seconds
framesintegerNoDesired total frame count
seedintegerNoRandom seed
camera_fixedbooleanNoTry to keep the camera fixed
watermarkbooleanNoAdd a watermark
toolsobject[]NoModel tool configuration
safety_identifierstringNoBusiness 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

ItemDescription
type=texttext contains the video prompt
type=image_urlA public image URL or asset://asset-...
type=video_urlA publicly reachable reference video URL or an available material reference
type=audio_urlA publicly reachable WAV or MP3 URL
roleImage: 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
}
ItemDescription
FormatsWAV and MP3
MaximumThree audio references per request
CombinationAt least one image or video is required
URLMust be directly downloadable by the video service
RecommendationStandard 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"
StatusDescription
queuedQueued
runningProcessing
succeededCompleted successfully
failedFailed

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.mp4

Errors 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:

StatusScenario
200A material business error may still be returned; inspect Result.Error
400Invalid JSON, Action, version, or field
401Missing or invalid API Key
403Missing video, model, or material permission
404Task or material not found
502Upstream service failed or returned an unreadable response
503No 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

  1. Use asset://asset-... only for an asset owned by the current account and ready for use.
  2. Do not treat a successful create request as proof that the asset is ready; wait for Active.
  3. Make sure public image, video, and audio URLs can be downloaded directly by the video service.
  4. Store group IDs, asset IDs, validation tokens, and video task IDs for reuse, polling, and cleanup.
  5. Treat optional response fields as nullable because model capabilities differ.
  6. Confirm that no workflow still references a group before deleting it.
  7. Keep the API Key on the server and never log it.

See Authentication and API Keys and Model Discovery and Catalog.

On this page