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
idcan 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
idanduri. YingTu does not keep assets created through the API and has no list endpoint.
How it works#
- 1
Your server
Host the character image at a public HTTPS URL and call
POST /api/seedance-assets/virtual-humans. - 2
YingTu
Validates the key and body, creates the asset at Volcengine, and returns
202right away.statusis usuallyProcessingat this point. - 3
Your server
Stores the returned
idanduri, then callsGET /api/seedance-assets/virtual-humans/{id}every 5–10 seconds. - 4
Volcengine
Downloads and processes the image. The status becomes
Active(ready) orFailed(thefailureobject says why). - 5
Your server
Stops polling at
Activeand passes theurias areference_imageto LaoZhang's Seedance video API. Reuse the sameurifrom then on.
Basics#
| Item | Value | Notes |
|---|---|---|
| Base URL | https://yingtu.ai | All paths below are relative to this origin. |
| Authentication | Authorization: Bearer $LAOZHANG_API_KEY | Bearer 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 body | Content-Type: application/json | POST only. The body must be a JSON object of at most 16 KB (16,384 bytes) and may contain only the documented fields. |
| Responses | JSON | Every 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 from | Your server only | The 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
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.
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"
}'| Field | Type | Required | Description |
|---|---|---|---|
image_url | string | Yes | Public HTTPS URL of the character image. See Image URL rules below. |
name | string | No | A 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
ActiveorFailed. - Problems with the image itself (content, format, size) usually surface later as
status: "Failed". Occasionally Volcengine rejects the request at creation time, which returns 502upstream_error.
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:
| Field | Type | Description |
|---|---|---|
request_id | string | Trace ID for this request: req_ followed by 32 hex characters. Include it when you contact support. |
id | string | YingTu 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. |
object | string | Always seedance.asset. |
kind | string | Always virtual_human. |
name | string | Asset name. Empty string when there is none. |
status | string | Asset status, passed through from Volcengine: Processing, Active, or Failed. See Statuses. |
uri | string | The value you pass to video generation, in the form asset://asset-.... It never changes after creation. Use it exactly as returned. |
created_at | string | Optional. Creation time reported by Volcengine; omitted when Volcengine does not return it. |
updated_at | string | Optional. Last update time reported by Volcengine. |
failure | object | Optional. Present only when Volcengine reports an error: { "code": "...", "message": "..." }. Either field may be an empty string. |
Retrieve a virtual-human asset
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.
# 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 parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The id (ast_...) returned when the asset was created. Passing the uri or asset-... returns 403. |
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"
}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#
| status | Meaning | What to do |
|---|---|---|
Processing | Volcengine is downloading and processing the image. The create response usually shows this. | Poll every 5–10 seconds. Not usable for generation yet. |
Active | Processing finished; ready for video generation. | Stop polling and start generating with the uri. |
Failed | Processing 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:
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
}
}| HTTP | code | When it happens | What to do | Retryable |
|---|---|---|---|---|
| 400 | invalid_request | The 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 |
| 401 | invalid_api_key | No Authorization header, not in Bearer <key> form, or LaoZhang reports the key as invalid or disabled. | Check the key and the header format. | No |
| 403 | resource_not_owned | The 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 |
| 403 | capability_unavailable | The 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 |
| 413 | payload_too_large | The body is larger than 16 KB. | Shrink the body. Valid requests are far below the limit. | No |
| 415 | unsupported_media_type | A POST was sent without Content-Type: application/json. | Add Content-Type: application/json. | No |
| 429 | rate_limit_exceeded | The 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 |
| 500 | internal_error | Unexpected error inside YingTu. | Contact support with the request_id. A POST may have partly run, so a retry can create a duplicate. | No |
| 502 | upstream_error | Volcengine 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 |
| 503 | authentication_unavailable | LaoZhang key validation is temporarily unreachable. | Retry later. | Yes |
| 503 | service_unavailable | YingTu'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, andX-RateLimit-Reset(window reset time in Unix seconds). Error responses do not include these headers. - Over the limit you get 429
rate_limit_exceededwithRetry-Afterin seconds. Throttling by Volcengine also returns 429, but withoutRetry-After. - If this quota is too low for your volume, contact YingTu support.
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a
X-RateLimit-Limit: 12
X-RateLimit-Remaining: 11
X-RateLimit-Reset: 1790326800Using 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_urlitem to thecontentarray: seturlto the asset'suri(asset://...) androletoreference_image. - Only use assets whose status is
Active. Do not submit assets that are stillProcessingor haveFailed. - Reuse the same
uriin 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.
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.
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.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.