• Overview
  • eKYC Platform
  • Face Authentication
  • Vision Score
  • Behavioral Biometric (BehavioIQ)
    SDK Integration
  • SIM Verification
  • Mule Account Database
  • eKYC Core
  • Demo Apps
TrustVision API Documentation

API v1.0.0.3

Overview

The TrustVision Behavioral API provides server-side endpoints for device registration, behavioral data collection, and fraud scoring. There are two groups of endpoints:

  • Server-side endpoints (/v1/register_device, /v1/behavior/score) — called by your backend using Access Key authentication.
  • SDK endpoints (/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.

Base URL

https://tv-staging.trustingsocial.com
https://tv.trustingsocial.com

Authentication

Access Key Authentication (Server-side)

Used by: POST /v1/register_device, POST /v1/behavior/score

These endpoints use the standard TrustVision Access Key authentication. Each request must include:

HeaderRequiredDescription
AuthorizationYesTV <access_key_id>:<signature>
X-TV-TimestampYesCurrent timestamp in RFC 3339 format (e.g. 2024-01-15T10:30:00Z)
X-TV-Content-MD5NoBase64-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)

Customer User Device Authentication (SDK)

Used by: POST /v1/behavior/create_challenge, POST /v1/behavior/push_event_batch

These endpoints use device-level RSA authentication. Each request must include:

HeaderRequiredDescription
AuthorizationYesTV <customer_user_device_id>:<encrypted_secret_key>:<signature>
X-TV-TimestampYesCurrent timestamp in RFC 3339 format
X-Challenge-IDNoChallenge 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.

End-to-End Encryption (SDK endpoints)

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):

  • Request body is encrypted with AES-GCM using the secret key from the Authorization header.
  • Response body is encrypted with AES-GCM. The response key is encrypted with the device's RSA public key using RSA-OAEP-SHA256, and the response is signed with the server's RSA private key using RSA-PSS-SHA256.

Encrypted response format:

json
{
  "data": "<AES-GCM encrypted response>",
  "response_key": "<RSA-OAEP encrypted AES key>",
  "signature": "<RSA-PSS-SHA256 signature of response_key + data>"
}

Endpoints

1. Register Device

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

Request Body

FieldTypeRequiredDescription
key_pair_idstringYesUUID of the key pair credential used for encryption
device_public_keystringYesBase64-encoded RSA public key from the device (PKIX/DER format)
encrypted_secret_keystringNoRSA-OAEP encrypted AES key (required when protocol is encrypted)
cus_user_idstringYesYour unique identifier for the end user
device_idstringYesUnique identifier for the device
protocolstringNoEmpty string (plain) or encrypted. When encrypted, the device_public_key is AES-GCM encrypted and encrypted_secret_key is required

Example request (plain protocol):

json
{
  "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):

json
{
  "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"
}

Response

json
{
  "data": {
    "customer_user_device_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "success"
  }
}
FieldTypeDescription
data.customer_user_device_idstringUUID of the newly created customer user device
data.statusstringsuccess

2. Create Challenge

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)

Request Body

FieldTypeRequiredDescription
session_idstringYesUnique session identifier from the SDK

Example request (plaintext before encryption):

json
{
  "session_id": "session-xyz-789"
}

Response

FieldTypeDescription
challenge_idstringUUID of the created challenge
noncestring64-character hex string (256-bit), single-use nonce for the session
server_public_keystringBase64-encoded RSA-2048 public key (PKIX/DER format)

Example response (plaintext before encryption):

json
{
  "data": {
    "challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
    "nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
    "server_public_key": "MIIBIjANBgkqhk..."
  }
}

3. Push Event Batch

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)

Request Body

FieldTypeRequiredDescription
noncestringYesThe nonce received from create_challenge
batch_hashstringYesHash of the current batch data
prev_batch_hashstringNoHash of the previous batch (empty for the first batch)
seqintNoSequence number, starting from 0 for the first batch
dataobjectYesJSON object containing the batch event data

Example request (plaintext before encryption):

json
{
  "nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
  "batch_hash": "e3b0c44298fc1c149afbf4c8996fb924",
  "prev_batch_hash": "",
  "seq": 0,
  "data": {
    "events": [
      {
        "type": "touch",
        "timestamp": 1705312200000,
        "x": 150.5,
        "y": 320.0
      }
    ]
  }
}

Response

json
{
  "data": {
    "status": "success"
  }
}

Note: Batches must be pushed in sequential order. The server verifies the hash chain: each batch's prev_batch_hash must match the previous batch's batch_hash, and sequence numbers must be continuous.


4. Compute Behavior Score

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

How scoring works

Behavioral scoring follows an enrollment / verification model, like fingerprint enrollment:

  • First session (enrollment). The first scored challenge for a (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).
  • Subsequent sessions (verification). Each later session is compared against the enrolled baseline. The score answers: "how similar is this session's behavior to the enrolled behavior?"
behavior_scorerisk_levelis_fraud
700 – 1000LOWfalse
400 – 699MEDIUMfalse
100 – 399HIGHfalse
0 – 99CRITICALtrue

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.

Request Body

FieldTypeRequiredDescription
challenge_idstringYesUUID of the challenge from create_challenge
transaction_dataobjectNoTransaction context for enhanced fraud scoring
is_updatebooleanNoOverrides whether this call updates the user's behavioral profile (baseline). Omit for the default behavior. See Controlling profile updates.

transaction_data fields:

FieldTypeRequiredDescription
transaction_idstringNoUnique transaction identifier
transaction_timestampstringNoISO 8601 timestamp
transaction_amountnumberNoTransaction amount
currency_codestringNoISO 4217 currency code (e.g. VND)
transaction_typestringNoType of transaction
channelstringNoTransaction channel
transaction_statusstringNoStatus of the transaction
account_numberstringNoSender account number (encrypted at rest)
account_holder_namestringNoSender account holder name (encrypted at rest)
bank_codestringNoSender bank code (encrypted at rest)
beneficiary_account_numberstringNoRecipient account number (encrypted at rest)
beneficiary_account_holder_namestringNoRecipient account holder name (encrypted at rest)
beneficiary_bank_codestringNoRecipient 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_data is also visible to behavior policies (see Policy decision below). For example, a policy can require a step-up challenge when transaction_amount exceeds a threshold — including on first-session (enrollment) calls.

Controlling profile updates with is_update

By 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_updateEffect
omitted / nullDefault behavior. The first session enrolls the baseline; later sessions update the profile only when your deployment's policy opts in.
trueAlways update. This session is folded into the baseline regardless of policy. On a first session this enrolls the baseline as usual.
falseNever 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_update takes priority over the default policy-driven behavior at every step. Use false for read-only scoring that must not mutate the profile, and true to 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):

json
{
  "challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
  "is_update": false
}

Example request:

json
{
  "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"
  }
}

Response

FieldTypeDescription
data.request_idstringUUID of the scoring request (for tracking)
data.behavior_scoreintegerBehavior score (0 – 1000). Higher means more similar to the enrolled baseline (more trustworthy)
data.risk_levelstringLOW, MEDIUM, HIGH, or CRITICAL (see mapping table above)
data.is_fraudbooleantrue only when risk_level is CRITICAL. Omitted when false — treat absence as false
data.is_confidentbooleanScoring confidence flag. Omitted when false
data.groupsobjectPer-signal-group breakdown, keyed by group name (device_description, device_behavior, user_behavior), each {score, weight}
data.explainersarrayScoring explainers (may be empty)
data.more_detailsobjectPresent on first-session responses only: {"is_first_session": true, "embedding_count": N}
data.server_infosobjectServer processing metadata
decisionobjectFinal action verdict. Present only when the behavior policy check is enabled (see Policy decision)
evaluation_detailsobjectApplied-policy details. Present only when the behavior policy check is enabled
errorsarrayList of errors (only on failure; data.status is failure in that case)

Example response (verification session):

json
{
  "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):

json
{
  "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"):

json
{
  "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": "..."}]}, no data block. See Error Responses.

Policy decision (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:

json
{
  "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"
      }
    ]
  }
}
FieldTypeDescription
decision.final_actionstringALLOW, STEP_UP, REVIEW, or BLOCK — the recommended action for this session
decision.action_payloadobjectAction parameters, or null. For STEP_UP: {"challenge_type": "SOFT_OTP" \| "FACE_AUTHEN"}
evaluation_details.applied_policiesarrayPolicies that matched this session (empty array when no policy matched)
applied_policies[].policy_idstringStable policy identifier
applied_policies[].policy_namestringHuman-readable policy name
applied_policies[].policy_versionstringPolicy version string
applied_policies[].policy_outcomestringHow 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_levelDefault final_actionchallenge_type
LOWALLOW
MEDIUMSTEP_UPSOFT_OTP
HIGHSTEP_UPFACE_AUTHEN
CRITICALREVIEW

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 decision nor evaluation_details is present and the response keeps the legacy data-only shape. Integrations should ignore unknown top-level keys.


Integration Flow

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 -----------------|
  1. Your backend calls POST /v1/register_device to register the user's device and receives a customer_user_device_id.
  2. The SDK uses the device credentials to call POST /v1/behavior/create_challenge, which returns a challenge ID, nonce, and server public key.
  3. The SDK collects behavioral data (touch, typing, sensors) and pushes batches via POST /v1/behavior/push_event_batch.
  4. When the session is complete, your backend calls 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.
  5. On the user's first session, step 4 enrolls the behavioral baseline instead of scoring (more_details.is_first_session: true); real scoring starts from the second session.

Error Responses

All endpoints return errors in the following format:

json
{
  "errors": [
    {
      "message": "description of the error",
      "code": "error_code"
    }
  ]
}

Note: errors[].code is 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: Android TSSessionError.NetworkError.serverErrorCode, iOS ApiError.serverErrorCode, and Flutter PlatformException.details['serverCode'].

Common Error Codes

HTTP StatusCodeDescription
400invalid_paramMissing or invalid request parameter
400invalid_challenge_idChallenge ID is not a valid UUID
400challenge_not_foundChallenge does not exist
400challenge_access_deniedChallenge does not belong to this client
400invalid_challenge_statusChallenge has already been submitted or is invalid
400customer_user_device_not_foundThe challenge's device record does not exist
400customer_user_device_deactivatedThe challenge's device has been deactivated
400no_batch_eventsNo batch events found for this challenge
400invalid_batch_eventsBatch events failed hash chain or sequence validation
400missing_nonceNonce is missing from request
400invalid_nonceNonce does not match the challenge or is expired
400missing_challenge_idChallenge ID is missing from context
403access_deniedInvalid credentials, signature, or deactivated key
403request_time_too_skewedTimestamp too far from server time
500internal_server_errorInternal server error
503bootstrap_failedFirst-session enrollment failed (scoring engine unavailable / timed out). Retry
503bootstrap_wait_timeoutAnother request is enrolling this user's baseline and did not finish in time. Retry