Skip to main content

Seedance virtual-human asset API

Create a Seedance virtual-human asset from one public HTTPS image and get an asset:// URI. Once the asset is Active, pass the URI as a reference_image to Seedance video generation and the same character stays consistent across every video. All calls use a LaoZhang API key and run from your server.

Last updated: 2026-09-25 · checked line by line against the live API code

Overview#

Virtual-human assets are for AI-generated or fictional characters such as brand mascots, digital presenters, or game characters. Create the asset once and reference the same uri in every video to keep the character's look consistent.

If the image shows a real, identifiable person, use the real-person asset API instead. It requires that person's own consent and liveness check.

The API is asynchronous: the create call returns immediately, Volcengine processes the image in the background, and you poll until the status is Active.

Before you start#

  • A LaoZhang API key. The asset API only checks that the key is valid and accepts any group, but generating videos with the asset requires a SeeDance2-group key. Use the same SeeDance2 key for both, because an asset id can only be queried with the key that created it.
  • A server of your own. Browsers cannot call the API directly, and the key must stay out of front-end code.
  • An HTTPS image URL that anyone on the internet can download without logging in and without hotlink protection.
  • Persistent storage for the returned id and uri. YingTu does not keep assets created through the API and has no list endpoint.

How it works#

  1. 1

    Your server

    Host the character image at a public HTTPS URL and call POST /api/seedance-assets/virtual-humans.

  2. 2

    YingTu

    Validates the key and body, creates the asset at Volcengine, and returns 202 right away. status is usually Processing at this point.

  3. 3

    Your server

    Stores the returned id and uri, then calls GET /api/seedance-assets/virtual-humans/{id} every 5–10 seconds.

  4. 4

    Volcengine

    Downloads and processes the image. The status becomes Active (ready) or Failed (the failure object says why).

  5. 5

    Your server

    Stops polling at Active and passes the uri as a reference_image to LaoZhang's Seedance video API. Reuse the same uri from then on.

Basics#

ItemValueNotes
Base URLhttps://yingtu.aiAll paths below are relative to this origin.
AuthenticationAuthorization: Bearer $LAOZHANG_API_KEYBearer is case-sensitive and must be followed by one space and the full key. A missing or malformed header returns 401 invalid_api_key.
Request bodyContent-Type: application/jsonPOST only. The body must be a JSON object of at most 16 KB (16,384 bytes) and may contain only the documented fields.
ResponsesJSONEvery success and error response includes request_id, also sent as the X-Request-Id header. All responses carry Cache-Control: no-store.
Where to call fromYour server onlyThe API sends no CORS headers, so browsers block direct calls, and your key must never ship to a browser anyway.

YingTu validates your key by calling LaoZhang's /v1/models with it and caches a successful result for 5 minutes per server instance. A key you just disabled can therefore keep working for up to 5 minutes. If LaoZhang's validation service is unreachable you get 503 authentication_unavailable; retry later.

Endpoints#

Create a virtual-human asset

POST/api/seedance-assets/virtual-humans

Submits one image and creates one virtual-human asset. The endpoint returns 202 Accepted as soon as the asset exists at Volcengine; it does not wait for processing. Every call creates a new asset (there is no idempotency key) and uses one of your 12 hourly writes.

ShellRequest
curl -X POST https://yingtu.ai/api/seedance-assets/virtual-humans \
  -H "Authorization: Bearer $LAOZHANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://cdn.example.com/characters/luna.png",
    "name": "luna-v1"
  }'
FieldTypeRequiredDescription
image_urlstringYesPublic HTTPS URL of the character image. See Image URL rules below.
namestringNoA label for your own reference, 1–64 characters after trimming. Omit the field if you do not need it; an empty string or null returns 400.

The body may contain only these two fields. Anything else returns 400 Unknown request field.

Image URL rules

  • Must start with https:// and must not contain a username or password (https://user:pass@...). Leading and trailing spaces are trimmed.
  • The host cannot be localhost, *.localhost, or *.local, and cannot be a private or reserved IP: 0.x, 10.x, 127.x, 169.254.x, 172.16.x–172.31.x, 192.168.x, or the IPv6 addresses ::, ::1, ::ffff:*, fc00::/7, fe80::/10.
  • YingTu only checks the URL format; it never downloads the image. Volcengine does. The URL therefore has to be downloadable anonymously from the public internet: no hotlink protection, no login, no cookies. If you use a pre-signed URL (S3, GCS, OSS, and so on), keep it valid until the asset reaches Active or Failed.
  • Problems with the image itself (content, format, size) usually surface later as status: "Failed". Occasionally Volcengine rejects the request at creation time, which returns 502 upstream_error.
HTTPResponse · 202 Accepted
HTTP/1.1 202 Accepted
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a
X-RateLimit-Limit: 12
X-RateLimit-Remaining: 11
X-RateLimit-Reset: 1790326800

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "id": "ast_9Qk2vX7mL0pT...cRz4w",
  "object": "seedance.asset",
  "kind": "virtual_human",
  "name": "luna-v1",
  "status": "Processing",
  "uri": "asset://asset-20260925080012-x7k2p"
}

The id in the example is shortened. Response fields:

FieldTypeDescription
request_idstringTrace ID for this request: req_ followed by 32 hex characters. Include it when you contact support.
idstringYingTu asset ID, starting with ast_. Use it only to check status. It is an encrypted token (usually around 250 characters, length varies) bound to the API key that created it. Store it as-is; do not truncate or parse it.
objectstringAlways seedance.asset.
kindstringAlways virtual_human.
namestringAsset name. Empty string when there is none.
statusstringAsset status, passed through from Volcengine: Processing, Active, or Failed. See Statuses.
uristringThe value you pass to video generation, in the form asset://asset-.... It never changes after creation. Use it exactly as returned.
created_atstringOptional. Creation time reported by Volcengine; omitted when Volcengine does not return it.
updated_atstringOptional. Last update time reported by Volcengine.
failureobjectOptional. Present only when Volcengine reports an error: { "code": "...", "message": "..." }. Either field may be an empty string.

Retrieve a virtual-human asset

GET/api/seedance-assets/virtual-humans/{id}

Returns the asset's current status. Every call asks Volcengine live and does not count toward the write quota. Only the key that created the asset can query it.

ShellRequest
# ASSET_ID is the id (ast_...) returned by the create call
curl https://yingtu.ai/api/seedance-assets/virtual-humans/$ASSET_ID \
  -H "Authorization: Bearer $LAOZHANG_API_KEY"
Path parameterTypeRequiredDescription
idstringYesThe id (ast_...) returned when the asset was created. Passing the uri or asset-... returns 403.
HTTPResponse · 200 OK · ready
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "id": "ast_9Qk2vX7mL0pT...cRz4w",
  "object": "seedance.asset",
  "kind": "virtual_human",
  "name": "luna-v1",
  "status": "Active",
  "uri": "asset://asset-20260925080012-x7k2p",
  "created_at": "2026-09-25T08:00:12Z",
  "updated_at": "2026-09-25T08:01:03Z"
}
HTTPResponse · 200 OK · processing failed
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "id": "ast_9Qk2vX7mL0pT...cRz4w",
  "object": "seedance.asset",
  "kind": "virtual_human",
  "name": "luna-v1",
  "status": "Failed",
  "uri": "asset://asset-20260925080012-x7k2p",
  "failure": {
    "code": "<code returned by Volcengine>",
    "message": "<message returned by Volcengine>"
  }
}

A failed asset still returns HTTP 200. Check the status field, not just the HTTP status. The fields are the same as in the create response.

Statuses#

statusMeaningWhat to do
ProcessingVolcengine is downloading and processing the image. The create response usually shows this.Poll every 5–10 seconds. Not usable for generation yet.
ActiveProcessing finished; ready for video generation.Stop polling and start generating with the uri.
FailedProcessing failed; failure holds Volcengine's reason.Fix the cause and create a new asset, which gets a new id and uri.

status is passed through from Volcengine unchanged. Treat any value not listed here as not ready and keep polling; if it does not change for a long time, contact support with the request_id. If YingTu cannot read the status right after creation, the create response also reports Processing.

Errors#

Every error uses the same shape. Look at both the HTTP status and error.code. error.retryable: true means an identical retry may succeed; false means you need to change the request or contact support. This example is a real response:

HTTPError response
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_42480b1cfd2147439b8c514ad8688ab0

{
  "error": {
    "code": "invalid_request",
    "message": "Unknown request field: consent_reference.",
    "request_id": "req_42480b1cfd2147439b8c514ad8688ab0",
    "retryable": false
  }
}
HTTPcodeWhen it happensWhat to doRetryable
400invalid_requestThe body is not a JSON object, a required field is missing, a value is invalid, or the body contains an undocumented field. message names the field.Fix the request as message describes. Retrying unchanged will not help.No
401invalid_api_keyNo Authorization header, not in Bearer <key> form, or LaoZhang reports the key as invalid or disabled.Check the key and the header format.No
403resource_not_ownedThe id was not created with this key, was truncated or altered, or a real-person id was sent to a virtual-human path. Unknown and not-yours both return 403; the API never returns 404 here.Use the same key that created it, the right path, and the ID exactly as returned.No
403capability_unavailableThe asset capability is unavailable on the Volcengine side. This is a problem with YingTu's service account, not your key.Contact support with the request_id.No
413payload_too_largeThe body is larger than 16 KB.Shrink the body. Valid requests are far below the limit.No
415unsupported_media_typeA POST was sent without Content-Type: application/json.Add Content-Type: application/json.No
429rate_limit_exceededThe key exceeded 12 writes per hour, or Volcengine throttled the request.If Retry-After is present, wait that many seconds. Without it (upstream throttling), back off exponentially.Yes
500internal_errorUnexpected error inside YingTu.Contact support with the request_id. A POST may have partly run, so a retry can create a duplicate.No
502upstream_errorVolcengine returned an error or incomplete data (for example no asset ID, or a verification link that failed YingTu's safety checks).Follow retryable: if true, retry later; if false, Volcengine usually rejected the request content, so change it first.Varies
503authentication_unavailableLaoZhang key validation is temporarily unreachable.Retry later.Yes
503service_unavailableYingTu's server configuration is temporarily unavailable.Retry later; contact support if it persists.Yes

Two responses do not use this JSON shape: a wrong HTTP method (for example GET on a create endpoint) returns 405 with no body, and an expired real-person callback relay shows the person's browser a 400 (see Verification callback).

Rate limits and tracing#

  • Each API key gets at most 12 write requests per hour, counted across all POST endpoints together: creating virtual-human assets, creating real-person verification sessions, and creating real-person assets. GET requests are not counted.
  • Every POST that passes key validation counts, including requests rejected with 400, 403, 409, 410, 413, or 415 and requests that fail upstream. 401 responses do not count. Validate parameters on your side before sending.
  • The window starts with the first POST in the hour and resets completely one hour later. It is not a sliding window.
  • Successful POST responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (window reset time in Unix seconds). Error responses do not include these headers.
  • Over the limit you get 429 rate_limit_exceeded with Retry-After in seconds. Throttling by Volcengine also returns 429, but without Retry-After.
  • If this quota is too low for your volume, contact YingTu support.
TextHeaders on a successful POST
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a
X-RateLimit-Limit: 12
X-RateLimit-Remaining: 11
X-RateLimit-Reset: 1790326800

Using the asset in Seedance video generation#

Videos are not generated by the YingTu asset API. Call LaoZhang's Seedance endpoint POST https://api.laozhang.ai/seedance/api/v3/contents/generations/tasks instead, with a key from the SeeDance2 group.

  • Add an image_url item to the content array: set url to the asset's uri (asset://...) and role to reference_image.
  • Only use assets whose status is Active. Do not submit assets that are still Processing or have Failed.
  • Reuse the same uri in as many tasks as you like; there is no need to create the asset again.
  • Reference images per task are capped: up to 9 for the Seedance 2.0 models and up to 30 for Seedance 2.5.
  • For every other parameter (model, ratio, duration, resolution, polling the task), follow the LaoZhang Seedance docs.
ShellcURL · create a video task
curl -X POST https://api.laozhang.ai/seedance/api/v3/contents/generations/tasks \
  -H "Authorization: Bearer $LAOZHANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao-seedance-2-0-fast-260128",
    "content": [
      { "type": "text", "text": "The character from the reference image walks slowly toward the camera on a rainy neon street at night, cinematic lighting" },
      {
        "type": "image_url",
        "image_url": { "url": "asset://asset-20260925080012-x7k2p" },
        "role": "reference_image"
      }
    ],
    "ratio": "16:9",
    "duration": 5,
    "resolution": "720p",
    "watermark": false,
    "generate_audio": false
  }'

A successful call returns { "id": "cgt-..." }; poll GET .../contents/generations/tasks/{id} for the result.

Assets created through this API do not appear among the account assets in the YingTu web workspace and cannot be selected there. Use them through LaoZhang's Seedance API.

Complete code samples#

Create an asset and wait until it is ready. Set the LAOZHANG_API_KEY environment variable first.

PythonPython · requests
import os
import time

import requests

BASE_URL = "https://yingtu.ai/api/seedance-assets"
HEADERS = {"Authorization": f"Bearer {os.environ['LAOZHANG_API_KEY']}"}


def api(method, path, **kwargs):
    resp = requests.request(method, BASE_URL + path, headers=HEADERS, timeout=60, **kwargs)
    body = resp.json()
    if not resp.ok:
        err = body["error"]
        raise RuntimeError(f"{resp.status_code} {err['code']}: {err['message']} ({err['request_id']})")
    return body


asset = api("POST", "/virtual-humans", json={
    "image_url": "https://cdn.example.com/characters/luna.png",
    "name": "luna-v1",
})
# Store id and uri in your database before polling
for _ in range(120):  # poll for up to ~10 minutes
    if asset["status"] in ("Active", "Failed"):
        break
    time.sleep(5)
    asset = api("GET", f"/virtual-humans/{asset['id']}")

if asset["status"] == "Active":
    print("Ready for generation:", asset["uri"])
else:
    print("Processing failed:", asset["status"], asset.get("failure"))
Node.jsJavaScript · Node.js 18+
// Node.js 18+. Save as virtual-human.mjs and run: node virtual-human.mjs
const BASE_URL = "https://yingtu.ai/api/seedance-assets";
const AUTH = { Authorization: "Bearer " + process.env.LAOZHANG_API_KEY };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function api(path, init = {}) {
  const res = await fetch(BASE_URL + path, {
    ...init,
    headers: { ...AUTH, ...init.headers },
    signal: AbortSignal.timeout(60_000),
  });
  const body = await res.json();
  if (!res.ok) {
    const { code, message, request_id } = body.error;
    throw new Error(res.status + " " + code + ": " + message + " (" + request_id + ")");
  }
  return body;
}

let asset = await api("/virtual-humans", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    image_url: "https://cdn.example.com/characters/luna.png",
    name: "luna-v1",
  }),
});
// Store asset.id and asset.uri in your database before polling
for (let i = 0; i < 120 && !["Active", "Failed"].includes(asset.status); i++) {
  await sleep(5000);
  asset = await api("/virtual-humans/" + asset.id);
}
console.log(asset.status, asset.status === "Active" ? asset.uri : asset.failure);

FAQ#

My asset keeps ending up Failed. What usually causes that?

Read failure.message first. Common causes: the image URL needs a login, has hotlink protection, or its signature expired, so Volcengine could not download it; the image content or format does not meet Volcengine's requirements; or the image shows a real person, which belongs in the real-person API.

What is the difference between id and uri? Which one do I store?

Store both. id (ast_...) is YingTu's lookup token: it only works with the GET endpoint and only with the key that created it. uri (asset://...) is the asset address at Volcengine, and it is what you pass to video generation.

How long should my database columns be?

ast_... values are usually around 250 characters and ver_... values usually 300 or more, and the length varies. Use an unbounded type such as TEXT, not VARCHAR(255), and do not assume a fixed format.

Why does an unknown ID return 403 instead of 404?

IDs are encrypted tokens and YingTu keeps no list of assets on its side. A token that cannot be decrypted, was altered, belongs to another key, or is sent to the wrong path always returns 403 resource_not_owned.

My POST timed out. Will retrying create a duplicate?

Yes. There is no idempotency key; every successful POST creates a new asset or session and uses one write. Create calls wait for Volcengine synchronously, so set your client timeout to 60 seconds or more. If a retry does create a duplicate, simply leave the extra asset unused.

What happens to existing assets if I rotate my API key?

IDs created with the old key return 403 when queried with the new key. The uri itself is not tied to a key. Before rotating, make sure your assets are Active and that you have stored their uri.

Is there a list or delete endpoint?

Not at the moment. YingTu does not store assets created through the API, so keep id, uri, and your own metadata in your system.

Can I call the API from a browser?

No. The API sends no CORS headers, so browsers block the call, and putting the key in front-end code would leak it. Call it from your server.

Do these endpoints cost anything?

The asset endpoints currently do not charge your LaoZhang balance. Video generation that uses the asset is billed at the normal Seedance price.

Changelog#

  • 2026-09-25: Rewrote the documentation after checking every statement against the live API code. Added the end-to-end flow, callback details, the full error table and the FAQ. API behavior did not change.