Create asset group (liveness)
curl --request POST \
--url https://api.tennda.ai/v1/visual-validate/session \
--header 'Authorization: <authorization>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"description": "<string>",
"channel_id": 123
}
'import requests
url = "https://api.tennda.ai/v1/visual-validate/session"
payload = {
"name": "<string>",
"description": "<string>",
"channel_id": 123
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': 'application/json'},
body: JSON.stringify({name: '<string>', description: '<string>', channel_id: 123})
};
fetch('https://api.tennda.ai/v1/visual-validate/session', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tennda.ai/v1/visual-validate/session",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'description' => '<string>',
'channel_id' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tennda.ai/v1/visual-validate/session"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tennda.ai/v1/visual-validate/session")
.header("Authorization", "<authorization>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tennda.ai/v1/visual-validate/session")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}"
response = http.request(request)
puts response.read_bodySeedance 2.0
Create asset group (liveness)
POST /v1/visual-validate/session (liveness_face)
POST
/
v1
/
visual-validate
/
session
Create asset group (liveness)
curl --request POST \
--url https://api.tennda.ai/v1/visual-validate/session \
--header 'Authorization: <authorization>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"description": "<string>",
"channel_id": 123
}
'import requests
url = "https://api.tennda.ai/v1/visual-validate/session"
payload = {
"name": "<string>",
"description": "<string>",
"channel_id": 123
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': 'application/json'},
body: JSON.stringify({name: '<string>', description: '<string>', channel_id: 123})
};
fetch('https://api.tennda.ai/v1/visual-validate/session', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tennda.ai/v1/visual-validate/session",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'description' => '<string>',
'channel_id' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tennda.ai/v1/visual-validate/session"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tennda.ai/v1/visual-validate/session")
.header("Authorization", "<authorization>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tennda.ai/v1/visual-validate/session")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"channel_id\": 123\n}"
response = http.request(request)
puts response.read_bodyIntroduction
Live-person asset groups (group_type: liveness_face) cannot be created via Create asset group. Call this endpoint first to start H5 liveness validation; the gateway persists the group after the user completes validation.
| Library | group_type | How to create |
|---|---|---|
| Virtual | aigc | POST /v1/asset-groups |
| Live person | liveness_face | H5 validation via this endpoint |
BytePlus
CreateVisualValidateSession does not accept name or description. After receiving upstream GroupId, the gateway calls UpdateAssetGroup to write those fields back to the BytePlus console.Authentication
Request body
Description; defaults to API key user
username if omitted or empty (same as Create asset group)H5 validation flow
1. Client starts validation → POST /v1/visual-validate/session
↓ returns { h5_link, state, ... }
2. Client opens h5_link in a popup; user completes validation
↓ BytePlus redirects to gateway /asset-validate-callback.html?state=…&bytedToken=…&resultCode=10000
3. Callback page fetches /v1/visual-validate/result; persists group_type=liveness_face
↓ window.opener.postMessage({ type: 'tennda-asset-validate-result', ok, group_id, … })
4. Client receives group_id; upload assets via POST /v1/assets (Image / Video / Audio)
POST /v1/visual-validate/result directly; the gateway callback page forwards state automatically.
Example
curl -X POST "https://api.tennda.ai/v1/visual-validate/session" \
-H "Authorization: Bearer sk-xxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name": "Live person A"}'
Response
{
"h5_link": "https://verify.byteplus.com/h5/?token=…&lang=zh-CN&lng=zh",
"state": "<base64url>.<hmac>",
"channel_id": 123,
"byted_token": "bp-token-xxxxxxxx",
"expires_in": 900
}
| Field | Description |
|---|---|
h5_link | BytePlus H5 liveness URL; gateway appends lang=zh-CN&lng=zh for Simplified Chinese by default |
state | Gateway-issued HMAC-SHA256 token bound to user / channel / group_name / byted_token; used by the callback page to fetch results |
byted_token | BytePlus credential for this validation session; verified against the value inside state on callback |
channel_id | Selected upstream channel |
expires_in | state token TTL in seconds, fixed at 900 (15 minutes). Not the H5 page lifetime—the BytePlus H5 link is valid for about 120 seconds; call this endpoint again after expiry |
Clients typically only need to open
h5_link in a popup and listen for window.message. On H5 timeout, close the popup, remove the listener, and call POST /v1/visual-validate/session again for a new link. Consider a client-side timeout of about 130s as a fallback.Client postMessage
The callback page (/asset-validate-callback.html) posts to window.opener (targetOrigin = '*'). If the callback page and parent are on different origins, identify messages by type rather than relying only on event.origin.
Success:
{
"type": "tennda-asset-validate-result",
"ok": true,
"group_id": "group-20260512083014-zyxwv",
"name": "Live person A",
"channel_id": 123,
"group_type": "liveness_face"
}
{
"type": "tennda-asset-validate-result",
"ok": false,
"result_code": "10003",
"error": "Liveness validation failed: face does not match reference"
}
const popup = window.open(session.h5_link, 'asset-validate', 'width=480,height=720');
const listener = (event) => {
const data = event.data;
if (!data || data.type !== 'tennda-asset-validate-result') return;
window.removeEventListener('message', listener);
if (data.ok) {
console.log('Asset group created:', data.group_id);
} else {
console.error('Validation failed:', data.error, data.result_code);
}
};
window.addEventListener('message', listener);
Next steps
- Use
group_idfrom postMessage (not from this endpoint’s synchronous response) with Create asset - Poll List assets or Get asset until
status: active - Reference assets with
asset://in Create video generation
?group_type=liveness_face).