Behavioral Biometrics (BehavioIQ)
Compute a behavior score
Finalizes a challenge and returns the behavior score and risk level for the session, plus the recommended action when the policy check is enabled.
Request
A server-side call that finalizes the challenge and triggers the scoring engine to analyze the collected behavioral data. Unlike the two SDK endpoints it takes plain JSON with Access Key authentication — no E2E encryption.
| Name | Type | Required | Description |
|---|---|---|---|
challenge_id | string | Required | UUID of the challenge from create_challenge |
transaction_data | object | Optional | Transaction context for enhanced fraud scoring. See Transaction data below. |
is_update | boolean | Optional | Overrides whether this call updates the user's behavioral profile (baseline). Omit for the default behavior. See below. |
The is_update parameter
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_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). |
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.
Read-only scoring (no profile update):
{
"challenge_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"is_update": false
}
Sample 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"
}
}
Response
data.behavior_score carries the verdict and data.risk_level bands it. 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") carryingmore_details.is_first_session: trueas 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_score | risk_level | is_fraud |
|---|---|---|
| 700 – 1000 | LOW | false |
| 400 – 699 | MEDIUM | false |
| 100 – 399 | HIGH | false |
| 0 – 99 | CRITICAL | true |
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 | 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) |
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": {}
}
}
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
}
}
}
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>"
}
]
}
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 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.
Failure codes
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.
The envelope is described under Error Responses, and the codes every Behavioral call can return are listed under Common Error Codes.
Transaction data
transaction_data carries the payment context behind a scored session. It is optional and purely additive: omitting it leaves the request, and the returned score, exactly as they are today. What it adds is context for the behavior-score policy rules configured for your account — those rules can read every field below, so a policy can, for example, require a step-up challenge when transaction_amount exceeds a threshold, including on first-session (enrollment) calls. See Policy decision above.
Every field is optional — send the ones you have. Fields marked encrypted at rest are stored encrypted by the server: send them in plaintext, and policy rules still evaluate them in plaintext.
Transaction core
| Field | Type | ISO 20022 path | Description |
|---|---|---|---|
transaction_id | string | PmtId/EndToEndId | Your identifier for the transaction |
transaction_timestamp | string | GrpHdr/CreDtTm | When the transaction was created, as an ISO 8601 timestamp |
transaction_amount | number | IntrBkSttlmAmt | Settlement amount |
currency_code | string | IntrBkSttlmAmt/@Ccy | ISO 4217 currency code, e.g. VND |
transaction_type | string | PmtTpInf | Coarse transaction type, e.g. transfer |
channel | string | — | Channel the transaction was initiated from, e.g. mobile_app |
transaction_status | string | pacs.002 TxSts | Status of the transaction |
Parties & accounts
| Field | Type | ISO 20022 path | Description |
|---|---|---|---|
account_number | string | DbtrAcct/Id | Sender account number — encrypted at rest |
account_holder_name | string | Dbtr/Nm | Sender account holder name — encrypted at rest |
bank_code | string | DbtrAgt/FinInstnId/(BICFI or ClrSysMmbId) | Sender bank code — encrypted at rest |
beneficiary_account_number | string | CdtrAcct/Id | Recipient account number — encrypted at rest |
beneficiary_account_holder_name | string | Cdtr/Nm | Recipient account holder name — encrypted at rest |
beneficiary_bank_code | string | CdtrAgt/FinInstnId/(BICFI or ClrSysMmbId) | Recipient bank code — encrypted at rest |
Risk & payment context
| Field | Type | ISO 20022 path | Description |
|---|---|---|---|
is_new_beneficiary | bool | — | Whether this is the first payment to this beneficiary |
days_since_beneficiary_added | int | — | Days since the beneficiary was added to the sender's payee list |
beneficiary_known_mule_flag | bool | — | Whether the beneficiary is on your known-mule list |
beneficiary_name_match_result | string | — | Outcome of the beneficiary name check (e.g. NAPAS name-check) |
beneficiary_proxy_alias | string | CdtrAcct/Prxy/Id | Beneficiary proxy alias, e.g. phone or VietQR alias — encrypted at rest |
beneficiary_country | string | Cdtr/PstlAdr/Ctry | Beneficiary country |
beneficiary_bank_country | string | CdtrAgt/FinInstnId/PstlAdr/Ctry | Beneficiary bank country |
beneficiary_account_open_date | string | — | When the beneficiary account was opened |
relationship_to_beneficiary | string | — | Declared relationship between sender and beneficiary |
direction | string | CdtDbtInd | Credit or debit indicator (CRDT / DBIT) |
purpose_code | string | Purp/Cd | Payment purpose code |
transaction_category | string | PmtTpInf/CtgyPurp/Cd | Category purpose code |
transaction_remark | string | RmtInf/Ustrd | Unstructured remittance information / transfer note |
payment_rail | string | PmtTpInf/LclInstrm/Cd | Payment rail or local instrument |
initiation_method | string | — | How the payment was initiated |
merchant_category_code | string | MrchntCtgyCd | Merchant category code |
original_amount | number | InstdAmt | Instructed amount, before any conversion or fees |
napas_system_trace_ref | string | PmtId/ClrSysRef | Clearing-system reference |
global_tx_uuid | string | PmtId/UETR | End-to-end unique transaction reference (UETR) |
is_recurring | bool | — | Whether the payment is part of a recurring series |
sender_country | string | Dbtr/PstlAdr/Ctry | Sender country |
sender_account_open_date | string | — | When the sender account was opened |
sender_date_of_birth | string | Dbtr/Id/PrvtId/DtAndPlcOfBirth/BirthDt | Sender date of birth — encrypted at rest |
customer_risk_level | string | — | Your own risk rating for the customer |
balance_before | number | — | Account balance before the transaction |
balance_after | number | — | Account balance after the transaction |
time_since_last_inflow | int | — | Seconds since the last credit into the account |
velocity_features | json_object | — | Free-form velocity features you compute for the customer |
txn_velocity_window | json_object | — | Free-form transaction counts/amounts over your own time windows |
failed_auth_attempts | int | — | Failed authentication attempts leading up to this transaction |
days_since_credential_change | int | — | Days since the customer last changed a credential |
sim_swap_recent_flag | bool | — | Whether a recent SIM swap was detected for the customer |
authentication_method | string | — | Authentication method used for the payment itself |
biometric_required_flag | bool | — | Whether your own policy already requires biometrics for this transaction |
ip_address | string | — | IP address the transaction was initiated from |
Field naming: canonical names and ISO 20022 paths
Each field can be keyed by either its canonical name (the Field column above) or the ISO 20022 path shown next to it; both are normalized to the canonical name on ingest. If the same logical field arrives under both keys in one request, the ISO-keyed value wins.
- Canonical names are matched case-insensitively, so
channelandChannelboth bind. ISO paths must match exactly as written above. Acct/OpngDtandBal/Amtare deliberately not accepted as aliases — each maps to two canonical fields (sender vs. beneficiary open date, balance before vs. after) and cannot be told apart from the key alone. Use the canonical names for those four fields.- Keys that are not listed above are kept verbatim and stay readable by policy rules, so bank-specific fields can be sent alongside the standard ones.