Asset Library
Upload images / videos / audio once, reference them via asset:// in video generation and other multimodal endpoints
Video generation and other multimodal endpoints accept references to files you've uploaded to the Asset Library. Upload a file once, then use asset://<ASSET_ID> anywhere a URL is expected. The format is identical on both the China and Overseas editions.
Compared to sending a 1-hour pre-signed URL on every request, asset library references give you:
- Permanent references — drafts won't expire mid-save
- Reuse across generations — same reference image, many requests
- Reuse across models — attach the same asset to different tasks
Three Steps
1. Create an AssetGroup (first time only)
Every asset hangs off an AssetGroup. A single user can own multiple groups.
curl -X POST https://api.xrtoken.ai/v1/asset-groups \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "Name": "my-assets" }'Response:
{ "GroupId": "asset-group-20260423-abc12", "Name": "my-assets" }Keep the GroupId around — you reuse it every time you add an asset.
One API key serving many end-users: add external_user_id in the body. See Third-party SaaS.
2. Get a URL for the file
Two options:
Option A — upload via our /v1/files endpoint (recommended; OpenAI-compatible):
curl -X POST https://api.xrtoken.ai/v1/files \
-H "Authorization: Bearer $XRT_API_KEY" \
-F 'file=@/path/to/reference.png' \
-F 'purpose=assistants'Response:
{
"id": "file-xxxxxxxx",
"object": "file",
"url": "https://xrtoken-storage-*.tos-*.bytepluses.com/uploads/<user>/<uuid>/reference.png?X-Tos-...",
"bytes": 123456,
"filename": "reference.png",
"purpose": "assistants"
}The returned url is a 24h presigned URL — pass it as the URL field in the next step. If you don't register within 24h, fetch a fresh signature via GET /v1/files/{id}.
Option B — pass any publicly-GET-able URL you already host (must return raw image/video bytes, not HTML).
3. Register the URL as an Asset
curl -X POST https://api.xrtoken.ai/v1/assets \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"GroupId": "asset-group-20260423-abc12",
"URL": "<URL from step 2>",
"AssetType": "Image",
"Name": "female lead reference",
"Moderation": { "Strategy": "Skip" }
}'AssetType can be Image / Video / Audio. Moderation.Strategy=Skip bypasses upstream content moderation (Overseas only; the field is ignored on the China edition).
Response:
{ "AssetId": "asset-20260423183015-xk4m2", ... }4. Reference it from a generation request
Replace image_url.url / video_url.url / audio_url.url with asset://<AssetId>; leave everything else unchanged:
{
"model": "doubao-seedance-2-0-260128",
"content": [
{ "type": "text", "text": "place the female lead into this scene and have her walk toward camera" },
{
"type": "image_url",
"role": "reference_image",
"image_url": { "url": "asset://asset-20260423183015-xk4m2" }
},
{
"type": "image_url",
"role": "reference_image",
"image_url": { "url": "asset://asset-20260419234137-txt6j" }
}
],
"duration": 5,
"ratio": "16:9",
"resolution": "720p"
}Where asset:// works
Only inside URL-shaped fields:
| Field | Purpose |
|---|---|
image_url.url | Reference image (reference_image), first frame (first_frame), last frame (last_frame) |
video_url.url | Reference video (reference_video, Seedance 2.0) |
audio_url.url | Reference audio (reference_audio, Seedance 2.0) |
Putting asset:// inside content[].text has no effect — it won't be resolved.
Mixing with raw URLs
Within a single request you can mix asset:// references and regular https:// URLs freely, e.g. one reference image from the library, another from a temporary signed URL:
"content": [
{ "type": "text", "text": "..." },
{ "type": "image_url", "role": "reference_image",
"image_url": { "url": "asset://asset-20260423183015-xk4m2" } },
{ "type": "image_url", "role": "reference_image",
"image_url": { "url": "https://your-cdn.com/temp/xxx.png" } }
]Limits
- At most 10 asset references per request (upstream Ark limit)
- Assets have a 90-day TTL (upstream Ark limit). Expired
asset://references fail to resolve — re-register the URL before day 90 for long-lived assets - Assets are scoped to the user who created them. Using someone else's
AssetIdreturns 403 - The
asset://prefix is only valid inside URL fields — putting it intextdoes nothing
Volcengine-native format (drop-in)
If you already integrate the asset library through the official Volcengine Ark SDK / Open API, point the base URL at us and swap the AK/SK signing helper for a Bearer token. Paths, parameters, and response shapes stay identical to the official API:
# Create asset (official Action=CreateAsset)
curl -X POST "https://api.xrtoken.ai/?Action=CreateAsset&Version=2024-01-01" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"GroupId": "group-2026**********-*****",
"URL": "https://example.com/image.jpg",
"Name": "test",
"AssetType": "Image",
"ProjectName": "default"
}'
# Get asset (official Action=GetAsset)
curl -X POST "https://api.xrtoken.ai/?Action=GetAsset&Version=2024-01-01" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "Id": "asset-20260423183015-xk4m2" }'ProjectName is optional: when omitted, the gateway project (currently xrtoken) is used automatically; when explicitly provided (including default), the value is passed through as-is. Name is optional too. Create, get, list, update, delete, and real-person verification all use the official POST /?Action=... path. Responses use the official Volcengine ResponseMetadata / Result envelope and keep the asset library's tenant isolation — you can only operate on assets under groups you own. See ARK SDK compatibility for details.
Third-party SaaS: external_user_id
You serve N end-users behind one API key. Pass that user's external_user_id on every call so asset groups and verified faces bind to that person. Others under the same key cannot see or use them.
Omit the field and the whole API key shares one pool (our Dashboard uses this).
Field rules
| Item | Value |
|---|---|
| Name | external_user_id |
| Meaning | Your own user id. We store it as an opaque string. |
| Length | Max 128 characters; longer values are truncated |
| POST / PUT | JSON body |
| GET / DELETE | Query: ?external_user_id=... |
| Auth | Your API key: Authorization: Bearer tr-xxx |
Use the same value on create, poll, list, get, update, and delete. Passing it when you open a session does not attach it to BytedToken. We do not store BytedToken → external_user_id.
CN host: https://api.xrtoken.net. Global: https://api.xrtoken.ai. Examples use $XRT_BASE.
Create groups / upload
curl -X POST "$XRT_BASE/v1/asset-groups" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"Name": "user-42 assets",
"external_user_id": "end-user-42"
}'curl "$XRT_BASE/v1/asset-groups?external_user_id=end-user-42" \
-H "Authorization: Bearer $XRT_API_KEY"Uploads take external_user_id in the body. GroupId must belong to that end-user. Video generation uses asset://<AssetId>.
Real-person scan: poll only (start here)
external_user_id on validate-session is not written onto BytedToken. Binding happens when validate-result succeeds. You must send it again on every poll.
Asset CRUD (create group, upload) does not need the next two items. Real-person scan requires trusted_creator plus enterprise verification — contact sales; not self-serve.
curl -X POST "$XRT_BASE/v1/asset-groups/validate-session" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "end-user-42"
}'Response (top-level or under Result):
{
"BytedToken": "eyJhbGci...",
"H5Link": "https://h5-v2.kych5.com?...",
"QRCodeDataURL": "data:image/png;base64,..."
}Put QRCodeDataURL in <img src>, or open H5Link. BytedToken lasts about 120 seconds. Poll every 5 seconds until success, failure, or timeout:
curl -X POST "$XRT_BASE/v1/asset-groups/validate-result" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bytedToken": "eyJhbGci...",
"external_user_id": "end-user-42"
}'| Response | Meaning |
|---|---|
GroupId + "status": "active" | Done. Group is bound to end-user-42. |
No GroupId | Still scanning. Keep polling. |
ResponseMetadata.Error | Failed. |
Omit external_user_id here and the group is bound as NULL. A later list with end-user-42 will not see it.
Real-person groups:
curl "$XRT_BASE/v1/asset-groups?type=real_person&external_user_id=end-user-42" \
-H "Authorization: Bearer $XRT_API_KEY"First-party scan flow: Real-person liveness.
Real-person scan: callback redirect
Use this if you have a landing page after the H5. No webhook to host. Send both fields when you open the session:
curl -X POST "$XRT_BASE/v1/asset-groups/validate-session" \
-H "Authorization: Bearer $XRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://your.app/face-done",
"external_user_id": "end-user-42"
}'callback_url must be http / https with a host. Your existing query string is kept.
After the scan, Volcengine hits our /v1/asset-groups/face-callback (do not call it yourself). We verify, fetch GroupId, bind with the external_user_id from create, then 302 to your page:
- Success:
https://your.app/face-done?status=success&group_id=group-...&external_user_id=end-user-42 - Failure:
https://your.app/face-done?status=failed(nogroup_id)
Omitting callback_url falls back to our dashboard — for first-party playground use.
Callback and poll may write the same group. If you also poll, still send the same external_user_id. If you omit it and poll wins the race, the row is NULL until callback fills it.
Do not
| What | What happens |
|---|---|
Pass the id only on validate-session, not on poll | Group bound as NULL; list by that id misses it |
| Different ids on create / poll / list | They cannot see each other |
| Put the id in a GET body | GET / DELETE only read the query string |
| List without the id | Isolation is off; you may see other end-users under the key |
Call /v1/asset-groups/face-callback yourself | Public Volcengine redirect target, not a client API |
Related API
- Create AssetGroup ·
POST /v1/asset-groups - List AssetGroups ·
GET /v1/asset-groups - Create Asset ·
POST /v1/assets - List Assets ·
GET /v1/assets - Delete Asset ·
DELETE /v1/assets/{id} - Open real-person session ·
POST /v1/asset-groups/validate-session - Poll real-person result ·
POST /v1/asset-groups/validate-result - Create Video Generation ·
POST /v1/videos/generations