Skip to main content

Seedance real-person asset API

Create a Seedance real-person asset for a real, identifiable person. Your server opens a verification session, the person gives consent and completes a liveness check on Volcengine's official page, and you then create the asset from their photo to get an asset:// URI for video generation. You do not sign in to YingTu, but the person must complete the check themselves.

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

Overview#

When a video has to show a real person (a host, spokesperson, or instructor), Seedance requires that person's own authorization; an ordinary photo is not enough. This API wraps the authorization into three steps: create a verification session, let the person verify, then create the asset.

The verification page is run by Volcengine (ByteDance's cloud platform) on ark.volcengine.com, and the person needs a device with a camera for the liveness check. YingTu never sees the person's ID documents or face data; it only relays the result.

The person must complete the verification themselves; nobody can do it on their behalf. For AI-generated or fictional characters, use the simpler virtual-human asset API.

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 throughout, because verification_id and asset id values can only be queried with the key that created them.
  • A server of your own. Browsers cannot call the API directly, and the key must stay out of front-end code.
  • A callback page (callback_url) at a public HTTPS URL. The person's browser lands there after verification. It is a web page for the person, not a webhook for your server.
  • A way to hand the verification link to the person, for example opening it in your app or website, or sending it by SMS or email.
  • A public HTTPS URL of a clear, front-facing photo of the person, plus persistent storage for verification_id, id, and uri.

How it works#

  1. 1

    Your server

    Calls POST /api/seedance-assets/real-persons/verifications with a callback_url, stores the returned verification_id next to your user, and hands the verification_url to the person.

  2. 2

    The person

    Opens verification_url within 30 minutes, gives consent, and completes the liveness check on Volcengine's official page.

  3. 3

    Volcengine → YingTu → the person's browser

    When the flow ends, the person's browser goes to YingTu's relay URL and is redirected (302) to your callback_url with verification_id and result_code appended.

  4. 4

    Your server

    When the callback page is opened (or on a timer), calls GET /api/seedance-assets/real-persons/verifications/{verification_id} until status is verified.

  5. 5

    Your server

    Before expires_at, calls POST /api/seedance-assets/real-persons with a clear, front-facing photo of the person and receives the asset id and uri (status Processing).

  6. 6

    Your server

    Polls GET /api/seedance-assets/real-persons/{id} until Active, then passes the uri as a reference_image to video generation.

The callback is a browser redirect, not a server notification. YingTu never sends webhooks to your server. If the person closes the page after verifying, your callback page never opens, so you must be able to get the result by polling the GET endpoint.

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.
TimestampsISO 8601expires_at is in UTC, for example 2026-09-25T08:30:00.000Z.

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 verification session

POST/api/seedance-assets/real-persons/verifications

Creates a verification session for one real person. Every call creates a new session and uses one write. Give verification_url to the person; keep verification_id on your server.

ShellRequest
curl -X POST https://yingtu.ai/api/seedance-assets/real-persons/verifications \
  -H "Authorization: Bearer $LAOZHANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://app.example.com/seedance/verified?user=42",
    "language": "en"
  }'
FieldTypeRequiredDescription
callback_urlstringYesYour page that the person's browser is sent to after verification. Must be a public HTTPS URL, with the same rules as image_url (see Create a real-person asset). Your own query parameters (such as a user or order ID) are kept on redirect, but parameters named verification_id or result_code are overwritten.
languagestringNoLanguage of the verification page: zh (Simplified Chinese, default), en, or zh-Hant. Case-sensitive; any other value returns 400. Volcengine offers only these three, so use en for Russian, Japanese, Korean, or Spanish speakers.

The body may contain only these two fields.

HTTPResponse · 201 Created
HTTP/1.1 201 Created
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",
  "verification_id": "ver_Hq3nW8bYt5...Kd0sE",
  "status": "requires_user_action",
  "verification_url": "https://ark.volcengine.com/...&lng=en",
  "callback_url": "https://app.example.com/seedance/verified?user=42",
  "expires_at": "2026-09-25T08:30:00.000Z"
}
FieldTypeDescription
request_idstringTrace ID for this request.
verification_idstringSession ID starting with ver_. It is an encrypted token (usually 300+ characters, length varies) bound to your key. Keep it on your server; you need it to check the result and to create the asset.
statusstringAlways requires_user_action: waiting for the person to verify.
verification_urlstringVolcengine's official verification page (https://ark.volcengine.com/...) with lng set from language. Give it only to the person; do not log it or put it on a public page.
callback_urlstringThe callback URL you sent, after URL normalization.
expires_atstringWhen verification_id expires (UTC, 30 minutes after creation). After that, checking status and creating the asset both return 410.

Check verification status

GET/api/seedance-assets/real-persons/verifications/{verification_id}

Tells you whether the person has passed. Every call asks Volcengine live and does not count toward the write quota. Check once when your callback page opens; if it is not verified yet, poll about every 5 seconds until verified or expiry.

ShellRequest
# VERIFICATION_ID is the verification_id (ver_...) you stored
curl https://yingtu.ai/api/seedance-assets/real-persons/verifications/$VERIFICATION_ID \
  -H "Authorization: Bearer $LAOZHANG_API_KEY"
Path parameterTypeRequiredDescription
verification_idstringYesThe verification_id from the create response, or the one appended to your callback URL. Either works.
HTTPResponse · 200 OK · not passed yet
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "verification_id": "ver_Hq3nW8bYt5...Kd0sE",
  "status": "pending",
  "expires_at": "2026-09-25T08:30:00.000Z"
}
HTTPResponse · 200 OK · passed
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "verification_id": "ver_Hq3nW8bYt5...Kd0sE",
  "status": "verified",
  "expires_at": "2026-09-25T08:30:00.000Z"
}
FieldTypeDescription
request_idstringTrace ID for this request.
verification_idstringEchoes the value you sent.
statusstringpending or verified. See Statuses.
expires_atstringWhen this verification_id expires (UTC).

pending comes back with HTTP 200; it is not an error. After expiry this endpoint returns 410 verification_expired instead of a status.

Create a real-person asset

POST/api/seedance-assets/real-persons

After verification passes, creates the real-person asset from the person's photo. Call it before verification_id expires. It returns 202, the asset is processed in the background, and you check its status just like a virtual-human asset. Every call uses one write.

ShellRequest
# VERIFICATION_ID is the verification_id (ver_...) you stored
curl -X POST https://yingtu.ai/api/seedance-assets/real-persons \
  -H "Authorization: Bearer $LAOZHANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "verification_id": "'"$VERIFICATION_ID"'",
    "image_url": "https://cdn.example.com/people/presenter-a.jpg",
    "name": "presenter-a"
  }'
FieldTypeRequiredDescription
verification_idstringYesA session that is already verified, created with the same key.
image_urlstringYesPublic HTTPS URL of a clear, front-facing photo of the person who completed the verification.
namestringNoAsset name, 1–64 characters after trimming. Omit it if you do not need it; an empty string or null returns 400.

The body may contain only these three fields. YingTu checks in this order: body format → image_url → whether the verification_id belongs to you (403) → expiry (410) → whether it has passed (409) → create the asset.

URL rules (apply to both image_url and callback_url)

  • 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: 10
X-RateLimit-Reset: 1790326800

{
  "request_id": "req_4f0c2a9e8b7d4c1fa3e56b7d9c0e1f2a",
  "id": "ast_9Qk2vX7mL0pT...cRz4w",
  "object": "seedance.asset",
  "kind": "real_person",
  "name": "presenter-a",
  "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 real_person.
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 real-person asset

GET/api/seedance-assets/real-persons/{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. Asset IDs do not expire; an expired verification session does not affect assets already created from it.

ShellRequest
# ASSET_ID is the id (ast_...) returned by the create call
curl https://yingtu.ai/api/seedance-assets/real-persons/$ASSET_ID \
  -H "Authorization: Bearer $LAOZHANG_API_KEY"
Path parameterTypeRequiredDescription
idstringYesThe id (ast_...) returned when the asset was created. Passing a verification_id, 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": "real_person",
  "name": "presenter-a",
  "status": "Active",
  "uri": "asset://asset-20260925080012-x7k2p",
  "created_at": "2026-09-25T08:00:12Z",
  "updated_at": "2026-09-25T08:01:03Z"
}

A failed asset still returns HTTP 200, with status: "Failed" and a failure object. Check the status field, not just the HTTP status.

Statuses#

Verification session

StatusWhere you see itMeaningWhat to do
requires_user_actionOnly in the create-session responseThe session exists and is waiting for the person to open the link.Hand verification_url to the person.
pendingGET status checkNo passing result yet. Not opened, in progress, abandoned, and not passed all show as pending.Keep polling until expires_at. If it never passes, create a new session so the person can try again.
verifiedGET status checkThe person passed.Call POST /real-persons before expires_at.
HTTP 410GET status check and asset creationThe verification_id expired (verification_expired).Create a new session. Assets already created are not affected.

There is no "failed" verification status. If the person does not pass, the status stays pending until it expires. You can use the callback's result_code to warn the person earlier, but whether they passed is decided only by the GET endpoint returning verified.

Asset

statusMeaningWhat to do
ProcessingVolcengine is processing the photo. 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 the asset again while the verification is still valid; after expiry the person has to verify again.

Asset status is passed through from Volcengine unchanged. Treat any value not listed here as not ready and keep polling.

Verification callback#

When the session is created, YingTu gives Volcengine its own relay URL, not your callback_url. When the person finishes, Volcengine sends their browser to the relay URL, and YingTu redirects the browser (302) to your callback_url:

TextRedirect chain
# 1) Volcengine sends the person's browser to YingTu's relay URL
GET https://yingtu.ai/api/seedance-assets/real-persons/callback/cb_...?bytedToken=...&resultCode=10000

# 2) YingTu answers with a redirect to your callback_url
HTTP/1.1 302 Found
Location: https://app.example.com/seedance/verified?user=42&verification_id=ver_Zp7cM1xRa4...Ty9vB&result_code=10000
Appended parameterDescription
verification_idA freshly issued session ID, also bound to your key. Normally it points to the same verification as the one you received at creation and works for status checks and asset creation. The string differs from the original (each encryption is different), so never compare the two as strings. It is only appended when Volcengine returns its session token.
result_codeVolcengine's result code, passed through unchanged; YingTu does not interpret it. Only appended when Volcengine returns one.
  • Treat the callback as a signal that the person is back. Anyone can forge the query string, so never decide pass or fail from result_code; always confirm verified with the GET endpoint.
  • Check status with the verification_id you stored when creating the session, and use your own callback_url parameters (such as user=42) to find the matching record.
  • Volcengine's session token (bytedToken) never appears in the redirected URL.
  • The relay URL stops working 35 minutes after the session was created. After that the person sees HTTP 400 with {"error":"Invalid or expired verification callback."} and is not redirected to your page.
  • The callback page is for the person. Show something like "Verification submitted, confirming…" and let your server check the result.

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 or verification_id was not created with this key, was truncated or altered, or a virtual-human id was sent to a real-person 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
409verification_pendingPOST /real-persons was called with a verification_id that has not passed yet.Poll the GET endpoint until verified, then create. Do not retry the POST in a loop: every POST uses write quota.Yes
410verification_expiredThe verification_id is past its expires_at. Returned by both the status check and asset creation.Create a new verification session and have the person verify again.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.
  • Each real-person asset needs at least two POSTs (verification session + asset), so one key can create about 6 real-person assets per hour. Poll the GET endpoint while waiting for verification instead of retrying POSTs.
  • 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#

The whole flow from verification session to a ready asset. Set the LAOZHANG_API_KEY environment variable first. In production, step 2 is usually triggered by your callback page; the samples poll instead.

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


# 1. Create a verification session. Your own query parameters on
#    callback_url are kept when the person is redirected back.
session = api("POST", "/real-persons/verifications", json={
    "callback_url": "https://app.example.com/seedance/verified?user=42",
    "language": "en",
})
verification_id = session["verification_id"]  # store it in your database next to user 42
print("Send this link to the person:", session["verification_url"])

# 2. Wait for the person to pass. Trigger this when your callback page
#    is opened, or just poll. After expires_at this raises 410 verification_expired.
while api("GET", f"/real-persons/verifications/{verification_id}")["status"] != "verified":
    time.sleep(5)

# 3. Create the asset from a clear, front-facing photo of the same person
asset = api("POST", "/real-persons", json={
    "verification_id": verification_id,
    "image_url": "https://cdn.example.com/people/presenter-a.jpg",
    "name": "presenter-a",
})

# 4. Wait until the asset is ready
for _ in range(120):
    if asset["status"] in ("Active", "Failed"):
        break
    time.sleep(5)
    asset = api("GET", f"/real-persons/{asset['id']}")
print(asset["status"], asset["uri"], asset.get("failure"))
Node.jsJavaScript · Node.js 18+
// Node.js 18+. Save as real-person.mjs and run: node real-person.mjs
const BASE_URL = "https://yingtu.ai/api/seedance-assets";
const AUTH = { Authorization: "Bearer " + process.env.LAOZHANG_API_KEY };
const JSON_HEADERS = { "Content-Type": "application/json" };
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;
}

// 1. Create a verification session; your own callback_url parameters come back unchanged
const session = await api("/real-persons/verifications", {
  method: "POST",
  headers: JSON_HEADERS,
  body: JSON.stringify({
    callback_url: "https://app.example.com/seedance/verified?user=42",
    language: "en",
  }),
});
const verificationId = session.verification_id; // store it in your database next to user 42
console.log("Send this link to the person:", session.verification_url);

// 2. Wait for the person to pass; after expires_at this throws 410 verification_expired
while ((await api("/real-persons/verifications/" + verificationId)).status !== "verified") {
  await sleep(5000);
}

// 3. Create the asset from a clear, front-facing photo of the same person
let asset = await api("/real-persons", {
  method: "POST",
  headers: JSON_HEADERS,
  body: JSON.stringify({
    verification_id: verificationId,
    image_url: "https://cdn.example.com/people/presenter-a.jpg",
    name: "presenter-a",
  }),
});

// 4. Wait until the asset is ready
for (let i = 0; i < 120 && !["Active", "Failed"].includes(asset.status); i++) {
  await sleep(5000);
  asset = await api("/real-persons/" + asset.id);
}
console.log(asset.status, asset.uri, asset.failure);

FAQ#

Does the callback notify my server?

No. It is a 302 redirect of the person's browser; your server only receives a request if their browser actually opens the callback page. Always be able to get the result by polling the GET endpoint.

Why is the verification_id on the callback different from the one I got at creation?

YingTu issues a new encrypted token during the redirect, so the string differs, but it normally refers to the same verification and both work. Match users with your own callback_url parameters instead of comparing the two strings.

What status do I see if the person fails verification?

Still pending, until expires_at passes and the API returns 410. There is no failed state. The person can reopen the same link while it is valid, or you can create a new session.

Can I still use assets after the verification expires?

Yes. Expiry only affects the verification_id; the asset's id and uri never expire.

How many assets can one verification create?

YingTu does not cap it: until expires_at, every POST /real-persons with the same verified verification_id creates a new asset and uses one write. Whether each photo processes successfully is up to Volcengine.

Can I create the link now and send it later?

Better not. verification_id expires 30 minutes after creation, so create the session when the person is ready to verify.

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.