The TrustVision Behavioral API provides server-side endpoints for device registration, behavioral data collection, and fraud scoring. There are two groups of endpoints:
/v1/register_device, /v1/behavior/score) — called by your backend using Access Key authentication./v1/behavior/create_challenge, /v1/behavior/push_event_batch) — called by the Behavioral SDK from client devices using device-level authentication with end-to-end encryption.https://tv-staging.trustingsocial.com
https://tv.trustingsocial.com
Used by: POST /v1/register_device, POST /v1/behavior/score
These endpoints use the standard TrustVision Access Key authentication. Each request must include:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | TV <access_key_id>:<signature> |
X-TV-Timestamp | Yes | Current timestamp in RFC 3339 format (e.g. 2024-01-15T10:30:00Z) |
X-TV-Content-MD5 | No | Base64-encoded MD5 hash of the request body (POST requests with encryption) |
Signature computation:
string_to_sign = HTTP_METHOD + "\n" + URI + "\n" + X-TV-Timestamp + "\n" + Content-MD5
signature = HMAC-SHA256(secret_key, string_to_sign)
Used by: POST /v1/behavior/create_challenge, POST /v1/behavior/push_event_batch
These endpoints use device-level RSA authentication. Each request must include:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | TV <customer_user_device_id>:<encrypted_secret_key>:<signature> |
X-TV-Timestamp | Yes | Current timestamp in RFC 3339 format |
X-Challenge-ID | No | Challenge ID prefixed with TV , e.g. TV <challenge_id> (required for push_event_batch) |
Signature computation:
string_to_sign = HTTP_METHOD + "\n" + URI + "\n" + X-TV-Timestamp + "\n" + encrypted_secret_key + "\n" + Content-MD5
signature = RSA-PSS-SHA256(device_private_key, string_to_sign)
encrypted_secret_key: A random AES secret key encrypted with the server's RSA public key using RSA-OAEP-SHA256.signature: RSA-PSS-SHA256 signature using the device's private key.The two SDK endpoints (/v1/behavior/create_challenge, /v1/behavior/push_event_batch) use E2E encryption (the server-side /v1/behavior/score endpoint uses plain JSON with Access Key authentication — no E2E encryption):
Authorization header.Encrypted response format:
{
"data": "<AES-GCM encrypted response>",
"response_key": "<RSA-OAEP encrypted AES key>",
"signature": "<RSA-PSS-SHA256 signature of response_key + data>"
}
Register a client device for behavioral data collection. This is a server-side call that creates a customer_user_device record.
Endpoint: POST /v1/register_device
Authentication: Access Key
| Field | Type | Required | Description |
|---|---|---|---|
key_pair_id | string | Yes | UUID of the key pair credential used for encryption |
device_public_key | string | Yes | Base64-encoded RSA public key from the device (PKIX/DER format) |
encrypted_secret_key | string | No | RSA-OAEP encrypted AES key (required when protocol is encrypted) |
cus_user_id | string | Yes | Your unique identifier for the end user |
device_id | string | Yes | Unique identifier for the device |
protocol | string | No | Empty string (plain) or encrypted. When encrypted, the device_public_key is AES-GCM encrypted and encrypted_secret_key is required |
Example request (plain protocol):
{
"key_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"device_public_key": "MIIBIjANBgkqhk...",
"cus_user_id": "user-123",
"device_id": "device-abc-456"
}
Example request (encrypted protocol):
{
"key_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"device_public_key": "<AES-GCM encrypted device public key>",
"encrypted_secret_key": "<RSA-OAEP encrypted AES key>",
"cus_user_id": "user-123",
"device_id": "device-abc-456",
"protocol": "encrypted"
}
{
"data": {
"customer_user_device_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "success"
}
}
| Field | Type | Description |
|---|---|---|
data.customer_user_device_id | string | UUID of the newly created customer user device |
data.status | string | success |
Create a behavioral challenge session. The SDK calls this endpoint to initiate a data collection session and receive a challenge ID, nonce, and server public key for E2E encryption.
Endpoint: POST /v1/behavior/create_challenge
Authentication: Customer User Device
Encryption: E2E (request and response are encrypted)
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Unique session identifier from the SDK |
Example request (plaintext before encryption):
{
"session_id": "session-xyz-789"
}
| Field | Type | Description |
|---|---|---|
challenge_id | string | UUID of the created challenge |
nonce | string | 64-character hex string (256-bit), single-use nonce for the session |
server_public_key | string | Base64-encoded RSA-2048 public key (PKIX/DER format) |
Example response (plaintext before encryption):
{
"data": {
"challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"server_public_key": "MIIBIjANBgkqhk..."
}
}
Submit a batch of behavioral event data collected by the SDK. Multiple batches can be pushed per challenge, forming a hash chain for integrity verification.
Endpoint: POST /v1/behavior/push_event_batch
Authentication: Customer User Device (with X-Challenge-ID header)
Encryption: E2E (request and response are encrypted)
| Field | Type | Required | Description |
|---|---|---|---|
nonce | string | Yes | The nonce received from create_challenge |
batch_hash | string | Yes | Hash of the current batch data |
prev_batch_hash | string | No | Hash of the previous batch (empty for the first batch) |
seq | int | No | Sequence number, starting from 0 for the first batch |
data | object | Yes | JSON object containing the batch event data |
Example request (plaintext before encryption):
{
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"batch_hash": "e3b0c44298fc1c149afbf4c8996fb924",
"prev_batch_hash": "",
"seq": 0,
"data": {
"events": [
{
"type": "touch",
"timestamp": 1705312200000,
"x": 150.5,
"y": 320.0
}
]
}
}
{
"data": {
"status": "success"
}
}
Note: Batches must be pushed in sequential order. The server verifies the hash chain: each batch's
prev_batch_hashmust match the previous batch'sbatch_hash, and sequence numbers must be continuous.
Submit a challenge for behavior scoring. This is a server-side call that finalizes the challenge and triggers the scoring engine to analyze the collected behavioral data.
Endpoint: POST /v1/behavior/score
Authentication: Access Key
Behavioral scoring follows an enrollment / verification model, like fingerprint enrollment:
(cus_user_id, client) pair builds the user's behavioral baseline. No real scoring takes place — the response is a synthetic safe default (behavior_score: 1000, risk_level: "LOW") carrying more_details.is_first_session: true as the unambiguous marker. Defer first-session risk decisions to your other layers (KYC, device, biometric).behavior_score | risk_level | is_fraud |
|---|---|---|
| 700 – 1000 | LOW | false |
| 400 – 699 | MEDIUM | false |
| 100 – 399 | HIGH | false |
| 0 – 99 | CRITICAL | true |
Note: Calling this endpoint again for an already-scored challenge re-scores it against the current baseline (the call is not idempotent); each call creates its own
request_id.
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id | string | Yes | UUID of the challenge from create_challenge |
transaction_data | object | No | Transaction context for enhanced fraud scoring |
is_update | boolean | No | Overrides whether this call updates the user's behavioral profile (baseline). Omit for the default behavior. See Controlling profile updates. |
transaction_data fields:
| Field | Type | Required | Description |
|---|---|---|---|
transaction_id | string | No | Unique transaction identifier |
transaction_timestamp | string | No | ISO 8601 timestamp |
transaction_amount | number | No | Transaction amount |
currency_code | string | No | ISO 4217 currency code (e.g. VND) |
transaction_type | string | No | Type of transaction |
channel | string | No | Transaction channel |
transaction_status | string | No | Status of the transaction |
account_number | string | No | Sender account number (encrypted at rest) |
account_holder_name | string | No | Sender account holder name (encrypted at rest) |
bank_code | string | No | Sender bank code (encrypted at rest) |
beneficiary_account_number | string | No | Recipient account number (encrypted at rest) |
beneficiary_account_holder_name | string | No | Recipient account holder name (encrypted at rest) |
beneficiary_bank_code | string | No | Recipient bank code (encrypted at rest) |
Note: Fields marked "encrypted at rest" are automatically encrypted by the server before storage. You should send them in plaintext.
Tip:
transaction_datais also visible to behavior policies (see Policy decision below). For example, a policy can require a step-up challenge whentransaction_amountexceeds a threshold — including on first-session (enrollment) calls.
is_updateBy default, the user's behavioral profile (the enrolled baseline) is maintained automatically: the first session enrolls the baseline, and later sessions refresh it according to the policy configured for your deployment. The optional is_update flag lets your backend override this on a per-call basis.
is_update | Effect |
|---|---|
omitted / null | Default behavior. The first session enrolls the baseline; later sessions update the profile only when your deployment's policy opts in. |
true | Always update. This session is folded into the baseline regardless of policy. On a first session this enrolls the baseline as usual. |
false | Never update. No change is made to the profile. On a first session (no baseline yet) no baseline is built — the response is still the synthetic first-session shape but with more_details.embedding_count: 0. On a later session the session is scored normally but read-only (the baseline is left untouched). |
Note: Once set,
is_updatetakes priority over the default policy-driven behavior at every step. Usefalsefor read-only scoring that must not mutate the profile, andtrueto force-enroll a known-good session. Omit the field to keep the default behavior — existing integrations need no change.
Example — read-only scoring (no profile update):
{
"challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"is_update": false
}
Example request:
{
"challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"transaction_data": {
"transaction_id": "txn-001",
"transaction_timestamp": "2024-01-15T10:30:00Z",
"transaction_amount": 5000000,
"currency_code": "VND",
"transaction_type": "transfer",
"channel": "mobile_app",
"account_number": "1234567890",
"beneficiary_account_number": "0987654321"
}
}
| Field | Type | Description |
|---|---|---|
data.request_id | string | UUID of the scoring request (for tracking) |
data.behavior_score | integer | Behavior score (0 – 1000). Higher means more similar to the enrolled baseline (more trustworthy) |
data.risk_level | string | LOW, MEDIUM, HIGH, or CRITICAL (see mapping table above) |
data.is_fraud | boolean | true only when risk_level is CRITICAL. Omitted when false — treat absence as false |
data.is_confident | boolean | Scoring confidence flag. Omitted when false |
data.groups | object | Per-signal-group breakdown, keyed by group name (device_description, device_behavior, user_behavior), each {score, weight} |
data.explainers | array | Scoring explainers (may be empty) |
data.more_details | object | Present on first-session responses only: {"is_first_session": true, "embedding_count": N} |
data.server_infos | object | Server processing metadata |
decision | object | Final action verdict. Present only when the behavior policy check is enabled (see Policy decision) |
evaluation_details | object | Applied-policy details. Present only when the behavior policy check is enabled |
errors | array | List of errors (only on failure; data.status is failure in that case) |
Example response (verification session):
{
"data": {
"request_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"behavior_score": 850,
"risk_level": "LOW",
"groups": {
"device_description": { "score": 800, "weight": 0.2 },
"device_behavior": { "score": 860, "weight": 0.4 },
"user_behavior": { "score": 855, "weight": 0.4 }
},
"explainers": [],
"server_infos": {}
}
}
Example response (first session — enrollment):
{
"data": {
"request_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"behavior_score": 1000,
"is_fraud": false,
"risk_level": "LOW",
"groups": {
"device_description": { "score": 0, "weight": 0.2 },
"device_behavior": { "score": 0, "weight": 0.4 },
"user_behavior": { "score": 0, "weight": 0.4 }
},
"is_confident": false,
"explainers": [],
"more_details": {
"is_first_session": true,
"embedding_count": 1
}
}
}
Example response (scoring failure — HTTP 200, data.status: "failure"):
{
"data": {
"status": "failure",
"request_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"server_infos": {}
},
"errors": [
{
"message": "<error description from the scoring engine>",
"code": "<engine-defined error code>"
}
]
}
Note: scoring failures (engine-side) return HTTP 200 with
data.status: "failure"and engine-defined error codes. Request-validation errors (e.g.no_batch_events,challenge_not_found) return HTTP 4xx/503 with an errors-only body —{"errors": [{"code": "...", "message": "..."}]}, nodatablock. See Error Responses.
decision + evaluation_details)Deployments can enable a behavior-score policy check per client or access key (configured by TrustVision operators — contact your integration manager to enable it). When enabled, every /v1/behavior/score response — including first-session responses — carries two additional top-level keys next to data:
{
"data": { "...": "unchanged DS payload as above" },
"decision": {
"final_action": "STEP_UP",
"action_payload": { "challenge_type": "FACE_AUTHEN" }
},
"evaluation_details": {
"applied_policies": [
{
"policy_id": "bp_critical_face_authen",
"policy_name": "CRITICAL risk → Face Authentication",
"policy_version": "2026.06.04.1",
"policy_outcome": "OVERRIDDEN"
}
]
}
}
| Field | Type | Description |
|---|---|---|
decision.final_action | string | ALLOW, STEP_UP, REVIEW, or BLOCK — the recommended action for this session |
decision.action_payload | object | Action parameters, or null. For STEP_UP: {"challenge_type": "SOFT_OTP" \| "FACE_AUTHEN"} |
evaluation_details.applied_policies | array | Policies that matched this session (empty array when no policy matched) |
applied_policies[].policy_id | string | Stable policy identifier |
applied_policies[].policy_name | string | Human-readable policy name |
applied_policies[].policy_version | string | Policy version string |
applied_policies[].policy_outcome | string | How the policy adjusted the default action: NO_CHANGE, ESCALATED, DOWNGRADED, or OVERRIDDEN |
When no policy matches, final_action falls back to the default mapping from risk_level:
risk_level | Default final_action | challenge_type |
|---|---|---|
LOW | ALLOW | — |
MEDIUM | STEP_UP | SOFT_OTP |
HIGH | STEP_UP | FACE_AUTHEN |
CRITICAL | REVIEW | — |
Policies are authored per deployment and can reference the full scoring context, including transaction_data (e.g. "require face authentication when a first-session transaction exceeds 1,000,000 VND").
Backward compatibility: when the policy check is not enabled (the default), neither
decisionnorevaluation_detailsis present and the response keeps the legacydata-only shape. Integrations should ignore unknown top-level keys.
The complete behavioral scoring flow involves both server-side and SDK calls:
Your Backend SDK (on device) TrustVision API
| | |
|--- POST /v1/register_device -------------------------------->|
|<--------------- customer_user_device_id --------------------|
| | |
|--- (pass device credentials to SDK) ----------------------->|
| | |
| |-- POST /v1/behavior/ |
| | create_challenge --------->|
| |<---- challenge_id, nonce, |
| | server_public_key -----|
| | |
| |-- (collect behavioral data) |
| | |
| |-- POST /v1/behavior/ |
| | push_event_batch --------->|
| |<---- status: success --------|
| | |
| |-- (repeat for more batches) |
| | |
|--- POST /v1/behavior/score --------------------------------->|
|<------ behavior_score, risk_level, decision -----------------|
POST /v1/register_device to register the user's device and receives a customer_user_device_id.POST /v1/behavior/create_challenge, which returns a challenge ID, nonce, and server public key.POST /v1/behavior/push_event_batch.POST /v1/behavior/score with the challenge ID (and optional transaction_data) to get the behavior score, risk level, and — when the policy check is enabled — the recommended decision.final_action.more_details.is_first_session: true); real scoring starts from the second session.All endpoints return errors in the following format:
{
"errors": [
{
"message": "description of the error",
"code": "error_code"
}
]
}
Note:
errors[].codeis the semantic server error code. On the SDK-authenticated endpoints, the Behavioral SDKs surface this value directly on the error they raise so client code can branch on it: AndroidTSSessionError.NetworkError.serverErrorCode, iOSApiError.serverErrorCode, and FlutterPlatformException.details['serverCode'].
| HTTP Status | Code | Description |
|---|---|---|
| 400 | invalid_param | Missing or invalid request parameter |
| 400 | invalid_challenge_id | Challenge ID is not a valid UUID |
| 400 | challenge_not_found | Challenge does not exist |
| 400 | challenge_access_denied | Challenge does not belong to this client |
| 400 | invalid_challenge_status | Challenge has already been submitted or is invalid |
| 400 | customer_user_device_not_found | The challenge's device record does not exist |
| 400 | customer_user_device_deactivated | The challenge's device has been deactivated |
| 400 | no_batch_events | No batch events found for this challenge |
| 400 | invalid_batch_events | Batch events failed hash chain or sequence validation |
| 400 | missing_nonce | Nonce is missing from request |
| 400 | invalid_nonce | Nonce does not match the challenge or is expired |
| 400 | missing_challenge_id | Challenge ID is missing from context |
| 403 | access_denied | Invalid credentials, signature, or deactivated key |
| 403 | request_time_too_skewed | Timestamp too far from server time |
| 500 | internal_server_error | Internal server error |
| 503 | bootstrap_failed | First-session enrollment failed (scoring engine unavailable / timed out). Retry |
| 503 | bootstrap_wait_timeout | Another request is enrolling this user's baseline and did not finish in time. Retry |