XRToken API Docs

Asset Library

Upload images / videos / audio once, reference them via asset:// in video generation and other multimodal endpoints

API Configuration
After saving, the Try It panel below sends real requests with this key.
Base: api.xrtoken.ai

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:

FieldPurpose
image_url.urlReference image (reference_image), first frame (first_frame), last frame (last_frame)
video_url.urlReference video (reference_video, Seedance 2.0)
audio_url.urlReference 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 AssetId returns 403
  • The asset:// prefix is only valid inside URL fields — putting it in text does 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

ItemValue
Nameexternal_user_id
MeaningYour own user id. We store it as an opaque string.
LengthMax 128 characters; longer values are truncated
POST / PUTJSON body
GET / DELETEQuery: ?external_user_id=...
AuthYour 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"
  }'
ResponseMeaning
GroupId + "status": "active"Done. Group is bound to end-user-42.
No GroupIdStill scanning. Keep polling.
ResponseMetadata.ErrorFailed.

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 (no group_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

WhatWhat happens
Pass the id only on validate-session, not on pollGroup bound as NULL; list by that id misses it
Different ids on create / poll / listThey cannot see each other
Put the id in a GET bodyGET / DELETE only read the query string
List without the idIsolation is off; you may see other end-users under the key
Call /v1/asset-groups/face-callback yourselfPublic Volcengine redirect target, not a client API

On this page