iOS v1.0.0.3
Specifications
| Spec | Value |
|---|---|
| SDK Version | 1.0.3 |
| Min iOS | 13.0 |
| Swift | 5.5+ (async/await) |
| Xcode | 16.3 |
| Distribution | XCFramework |
| External Dependency | None (URLSession-based) |
Installation
XCFramework
- Get the
BehaviorSDK.xcframeworkfrom TrustVision - Drag it into your Xcode project under Frameworks, Libraries, and Embedded Content
- Set Embed to Embed & Sign
Permissions (Info.plist)
Add the following keys if you enable Bluetooth or Location collection:
<!-- Required only if canAskPermission is true and Bluetooth is used -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used for device behavioral analysis</string>
<!-- Required only if canAskPermission is true and Location is used -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used for device behavioral analysis</string>
Configuration
import BehaviorSDK
let config = TSBehavioralConfig(
appId: "your-app-id", // Your app's bundle identifier
baseUrl: "https://your-base-url/api", // Provided by TrustVision
metadataConfig: MetaDataCollectionConfig(
intervalMillis: 2000,
canAskPermission: false,
enabled: true
),
uploadBlockSize: 10,
sensorConfig: SensorConfig(
samplingIntervalMs: 20,
enabled: true
),
keyboardConfig: KeyboardConfig(
trackRawText: false,
enabled: true
),
touchConfig: TouchConfig(
enabled: true
),
screenTransitionBlockSize: 1,
appStateBlockSize: 1,
retryConfig: RetryConfig(
enabled: true,
maxRetries: 2,
retryDelayMs: 1000,
maxDelayMs: 5000,
maxRetryAttempts: 3,
maxRetryStorageItems: 50,
finalFlushTimeoutSeconds: 10,
retryItemDelaySeconds: 0.5
)
)
TSBehavioralConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
appId | String | Yes | — | Your app's bundle identifier |
baseUrl | String | Yes | — | Base URL for TrustVision API |
metadataConfig | MetaDataCollectionConfig | No | Default values | Device metadata collection settings |
uploadBlockSize | Int | No | 10 | Number of batch entries before auto-upload |
sensorConfig | SensorConfig | No | SensorConfig() | Sensor data collection settings |
keyboardConfig | KeyboardConfig | No | KeyboardConfig() | Keyboard tracking settings |
touchConfig | TouchConfig | No | TouchConfig() | Touch tracking settings |
screenTransitionBlockSize | Int | No | 1 | Screen transition batch size |
appStateBlockSize | Int | No | 1 | Number of app-state events per upload batch |
retryConfig | RetryConfig | No | RetryConfig() | Upload retry queue settings |
sslCertificates | [TSCertificate]? | No | nil | Certificates to pin. See SSL Pinning |
SSL Pinning
Optional certificate pinning. Pass pinned certificates via sslCertificates; when the array is non-empty the SDK trusts only servers presenting one of these certificates on every API call. nil or an empty array disables pinning and falls back to the system trust store. Each TSCertificate references a certificate file bundled in your app:
let config = TSBehavioralConfig(
appId: "com.example.app",
baseUrl: "https://api.trustingsocial.com",
sslCertificates: [
TSCertificate(name: "my_server_cert", type: "der")
]
)
TSCertificate(name:type:bundle:) loads <name>.<type> from the given bundle (defaults to .main).
MetaDataCollectionConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
intervalMillis | Int | Yes | — | Metadata collection interval in milliseconds |
canAskPermission | Bool | No | false | Whether SDK may request system permissions (Bluetooth, Location) |
enabled | Bool | No | true | Enable/disable metadata collection |
typeModes | [MetaDataType: MetaDataCollectionMode] | No | deviceDescription→manual, deviceBehaviors→interval, userBehaviors→interval | Collection mode per metadata type. .interval = repeating timer; .manual = explicit calls only |
collectOnceTypes | Set<MetaDataType> | No | [.deviceDescription] | Types collected once at session start and stop (fire-and-forget boundary snapshots) |
SensorConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
samplingIntervalMs | Int | No | 20 (50Hz) | Sensor sampling interval in milliseconds. Must be > 0 |
enabled | Bool | No | true | Enable/disable sensor collection |
KeyboardConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
trackRawText | Bool | No | false | Whether to collect raw typed text. When false, only character type is recorded |
enabled | Bool | No | true | Enable/disable keyboard tracking |
blockSize | Int | No | 1 | Number of keyboard events per upload batch (≥ 1) |
TouchConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Bool | No | true | Enable/disable touch tracking |
RetryConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Bool | No | true | Enable/disable the retry queue |
maxRetries | Int | No | 2 | Max immediate retries per request (0–10). Total calls = value + 1 |
retryDelayMs | Int | No | 1000 | Base delay before first retry in milliseconds (> 0) |
maxDelayMs | Int | No | 5000 | Maximum delay cap in milliseconds (≥ retryDelayMs) |
maxRetryAttempts | Int | No | 3 | Maximum retry-queue attempts before dropping a failed batch (≥ 1) |
maxRetryStorageItems | Int | No | 50 | Maximum items in retry storage before FIFO eviction (≥ 1) |
finalFlushTimeoutSeconds | TimeInterval | No | 10.0 | Timeout in seconds for flush at session end (≥ 1) |
retryItemDelaySeconds | TimeInterval | No | 0.5 | Delay between retry uploads in seconds (≥ 0) |
SDK Functions
TSBehavioralSDK.shared is a singleton instance. Uses Swift async/await for asynchronous operations. Some methods require @MainActor.
initialize
Initializes the SDK. Must be called first before any other method. Generates device identity (EC key pair stored in Keychain) and returns device information needed for registration.
If previously initialized, the existing session will be torn down and reinitialized.
Signature:
func initialize(config: TSBehavioralConfig) async throws -> TSDeviceInitInfo
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
config | TSBehavioralConfig | Yes | SDK configuration |
Returns: TSDeviceInitInfo
| Field | Type | Description |
|---|---|---|
deviceId | String | Unique device identifier (UUID) |
bootstrapKeyId | String | Key pair ID for encryption |
isRegistered | Bool | Whether device was previously registered |
encryptedDevicePublicKeyPayload | String | AES-GCM sealed device public key (base64). Pass to device_public_key in register API |
encryptedSecretKey | String | RSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API |
Throws: TSSessionError.keychainError, TSSessionError.cryptoError
Example:
Task {
do {
let initInfo = try await TSBehavioralSDK.shared.initialize(
config: config
)
print("Device ID: \(initInfo.deviceId)")
print("Already registered: \(initInfo.isRegistered)")
} catch let error as TSSessionError {
print("Init error [\(error.name)]: \(error.localizedDescription)")
}
}
markDeviceRegistered
Marks the device as registered after your backend has completed server-side registration with TrustVision. Must be called after initialize() and before startCollect().
Registration status is persisted in Keychain. On subsequent launches, initInfo.isRegistered will return true and you can skip the registration step.
Signature (Option A — parsed values):
func markDeviceRegistered(isRegistered: Bool, customerUserDeviceId: String) throws
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
isRegistered | Bool | Yes | Registration status (true to mark as registered) |
customerUserDeviceId | String | Yes | Unique user-device identifier returned by your server |
Signature (Option B — raw server response):
func markDeviceRegistered(encryptedPayload: Data) throws
| Parameter | Type | Required | Description |
|---|---|---|---|
encryptedPayload | Data | Yes | Raw response data from your server's registration API. SDK extracts customer_user_device_id from the JSON |
Throws: TSSessionError.keychainError
Example — using parsed values:
let initInfo = try await TSBehavioralSDK.shared.initialize(config: config)
if !initInfo.isRegistered {
// Your backend calls TrustVision API: POST /v1/register_device
// with: device_id, key_pair_id, device_public_key, encrypted_secret_key, cus_user_id, protocol
let userDeviceId = try await yourBackend.registerDevice(
deviceId: initInfo.deviceId,
keyPairId: initInfo.bootstrapKeyId,
devicePublicKey: initInfo.encryptedDevicePublicKeyPayload,
encryptedSecretKey: initInfo.encryptedSecretKey,
cusUserId: "your-user-id"
)
try TSBehavioralSDK.shared.markDeviceRegistered(
isRegistered: true,
customerUserDeviceId: userDeviceId
)
}
Example — using raw server response:
let responseData = try await yourBackend.registerDeviceRaw(initInfo)
// responseData is the raw Data from your server
// SDK parses: { "data": { "customer_user_device_id": "..." } }
try TSBehavioralSDK.shared.markDeviceRegistered(
encryptedPayload: responseData
)
startCollect
Creates a challenge with the TrustVision server and starts all data collectors (sensors, metadata, touch, keyboard). Requires device to be registered.
If a previous session is still active, it will be auto-stopped before starting a new one.
Signature:
@MainActor
func startCollect() async throws
Must be called on @MainActor (main thread).
Throws: TSSessionError.deviceNotRegistered, TSSessionError.networkError, TSSessionError.invalidChallengeResponse
Example:
Task { @MainActor in
do {
try await TSBehavioralSDK.shared.startCollect()
print("Collection started")
} catch let error as TSSessionError {
switch error {
case .deviceNotRegistered:
print("Register device first")
case .networkError(let underlyingError):
print("Network error: \(underlyingError)")
default:
print("Error: \(error.name)")
}
}
}
updateScreenTransition (Required)
Important: You must call
updateScreenTransition()immediately afterstartCollect()and whenever the user navigates to a new screen. This is required for behavioral data to be correctly associated with screen context.
Records a screen transition event. Non-throwing. Silently no-ops if SDK is not initialized or session is not started.
Signature:
func updateScreenTransition(screenName: String)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
screenName | String | Yes | Name of the screen being navigated to |
Example — call immediately after startCollect:
try await TSBehavioralSDK.shared.startCollect()
TSBehavioralSDK.shared.updateScreenTransition(screenName: "main_screen")
// Then call again on every screen navigation
TSBehavioralSDK.shared.updateScreenTransition(screenName: "payment_screen")
Example — base view controller pattern (recommended):
class BehavioralBaseVC: UIViewController {
var screenName: String? { nil }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let name = screenName {
TSBehavioralSDK.shared.updateScreenTransition(
screenName: name
)
}
}
}
class PaymentVC: BehavioralBaseVC {
override var screenName: String? { "payment" }
}
class InputPinVC: BehavioralBaseVC {
override var screenName: String? { "input_pin" }
}
startKeyboardTracking
Registers a UITextField or UITextView for typing data capture. Automatically tracks typing patterns (character type, timing, text length).
Note: Text fields with
isSecureTextEntry = trueare not supported. The SDK will not track secure text fields, as this would violate Apple's privacy policy. Do not pass secure text fields to this method.
Must be called on @MainActor. Non-throwing. Silently no-ops if SDK is not initialized.
Signatures:
@MainActor func startKeyboardTracking(textField: UITextField)
@MainActor func startKeyboardTracking(textView: UITextView)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
textField | UITextField | Yes | Text field to track |
textView | UITextView | Yes | Text view to track |
Example:
class FormVC: UIViewController {
@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var nameField: UITextField!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Only track non-secure text fields
TSBehavioralSDK.shared.startKeyboardTracking(textField: emailField)
TSBehavioralSDK.shared.startKeyboardTracking(textField: nameField)
}
}
stopKeyboardTracking
Unregisters a specific UITextField or UITextView from typing tracking. Non-throwing. Silently no-ops if SDK is not initialized.
Signatures:
func stopKeyboardTracking(textField: UITextField)
func stopKeyboardTracking(textView: UITextView)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
textField | UITextField | Yes | Text field to stop tracking |
textView | UITextView | Yes | Text view to stop tracking |
Example:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
TSBehavioralSDK.shared.stopKeyboardTracking(textField: emailField)
TSBehavioralSDK.shared.stopKeyboardTracking(textField: passwordField)
}
addKeyboardTrackingData
Sends keyboard tracking data manually. Use this for cross-platform integrations (Flutter, React Native) where native UIKit tracking is not available.
Non-throwing. Silently no-ops if SDK is not initialized.
Signature:
func addKeyboardTrackingData(_ data: TSTypingData)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
data | TSTypingData | Yes | Keyboard event data |
TSTypingData fields:
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | Int64 | Yes | Event timestamp in milliseconds since epoch |
textLength | Int? | No | Current text field length |
data | String? | No | Character value (only if trackRawText is true) |
type | TSKeyboardDataType | Yes | Type of keystroke |
viewId | String | Yes | Identifier for the input field |
TSKeyboardDataType values: lowerText, upperText, number, spacing, specialCharacters, delete, cursorMoving, unknown, mixed
Example:
TSBehavioralSDK.shared.addKeyboardTrackingData(
TSTypingData(
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
textLength: 5,
data: nil,
type: .lowerText,
viewId: "email_field"
)
)
trackTextChange
Stateless, framework-agnostic keyboard tracking entry point for SwiftUI text fields where UITextField/UITextView delegates are unavailable. Pass old and new text on every onChange — the SDK computes the diff and classifies the keystroke type internally.
Non-throwing. Silently no-ops if SDK is not initialized.
Signature:
func trackTextChange(oldText: String, newText: String, viewId: String)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
oldText | String | Yes | Text value before the change |
newText | String | Yes | Text value after the change |
viewId | String | Yes | Stable identifier for the input field |
Example — SwiftUI (iOS 14–16):
struct LoginView: View {
@State private var email = ""
var body: some View {
TextField("Email", text: $email)
.onChange(of: email) { newValue in
TSBehavioralSDK.shared.trackTextChange(
oldText: email,
newText: newValue,
viewId: "email_field"
)
}
}
}
Example — SwiftUI (iOS 17+):
TextField("Email", text: $email)
.onChange(of: email) { oldValue, newValue in
TSBehavioralSDK.shared.trackTextChange(
oldText: oldValue,
newText: newValue,
viewId: "email_field"
)
}
trackTouchEvent
Records a touch event from cross-platform pointer data (Flutter, React Native). Non-throwing. Silently no-ops if SDK is not initialized.
For native iOS apps, touch tracking is handled automatically by the SDK when touchConfig.enabled is true.
Signature:
func trackTouchEvent(event: TSPointerEventDto)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event | TSPointerEventDto | Yes | Touch event data |
TSPointerEventDto fields:
| Field | Type | Description |
|---|---|---|
actionName | String? | Action name: "down", "move", "up" |
actionMasked | Int? | Action constant |
pointerId | Int? | Pointer identifier |
globalX | Double? | Screen X coordinate |
globalY | Double? | Screen Y coordinate |
Example:
TSBehavioralSDK.shared.trackTouchEvent(
event: TSPointerEventDto(
actionName: "down",
actionMasked: 0,
pointerId: 0,
globalX: 150.0,
globalY: 300.0
)
)
finalizeCollect
Stops all collectors, flushes remaining data, and waits for the upload queue to drain completely. This is the clean shutdown — ensures all collected data is uploaded before returning.
Pass the returned challengeId to your backend to trigger behavior score computation via POST /v1/behavior/score.
Signature:
@MainActor
func finalizeCollect() async throws -> TSFinalizeCollectResult
Must be called on @MainActor (main thread).
Returns: TSFinalizeCollectResult
| Field | Type | Description |
|---|---|---|
challengeId | String | Challenge ID — pass to backend for scoring |
nonce | String | Session nonce |
lastSeq | Int | Last uploaded batch sequence number |
batchId | String? | Last batch identifier (nil if no batches uploaded) |
droppedBatchCount | Int | Number of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize |
lastUploadError | String? | Description of the last upload error that caused a drop, or nil when none |
Observability (v1.0.0.3):
droppedBatchCountandlastUploadErrorlet you detect silent data loss after finalize. A non-zerodroppedBatchCountmeans some collected batches never reached the server; inspectlastUploadErrorfor the cause.
Throws: TSSessionError.notStarted
Example:
Task { @MainActor in
do {
let result = try await TSBehavioralSDK.shared.finalizeCollect()
print("Challenge ID: \(result.challengeId)")
print("Last seq: \(result.lastSeq)")
if result.droppedBatchCount > 0 {
print("Dropped \(result.droppedBatchCount) batches. Last error: \(result.lastUploadError ?? "nil")")
}
// Send challengeId to your backend for scoring
try await yourBackend.computeBehaviorScore(
challengeId: result.challengeId
)
} catch let error as TSSessionError {
print("Finalize error [\(error.name)]: \(error.localizedDescription)")
}
}
stopCollect
Stops all collectors immediately without flushing data or draining the upload queue. This is the fast shutdown — use when you need to stop quickly (e.g., app backgrounding). May result in partial data loss.
Signature:
@MainActor
func stopCollect() throws -> TSFinalizeCollectResult
Must be called on @MainActor (main thread). Synchronous (not async).
Returns: TSFinalizeCollectResult (same structure as finalizeCollect, including the droppedBatchCount and lastUploadError observability fields). Because stopCollect() skips the flush and upload-queue drain, a non-zero droppedBatchCount is expected here.
Throws: TSSessionError.notStarted
finalizeCollect() | stopCollect() | |
|---|---|---|
| Async | async throws | throws (sync) |
| Flush data | Yes | No |
| Drain upload queue | Yes (waits) | No |
| Data loss risk | None | Possible |
| Use case | Normal completion | App backgrounding |
Example:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
do {
let result = try TSBehavioralSDK.shared.stopCollect()
print("Stopped. Last seq: \(result.lastSeq)")
} catch {
print("Stop error: \(error)")
}
}
}
clearDeviceIdentity
Clears all stored device identity (device ID, registration status, server keys) from Keychain and session state. You must call initialize() again after this to generate a new device identity.
Use when the user logs out or you need to reset the SDK completely.
Signature:
func clearDeviceIdentity() throws
Throws: TSSessionError.keychainError
Example:
do {
try TSBehavioralSDK.shared.clearDeviceIdentity()
print("Device identity cleared")
// Must call initialize() again before using SDK
} catch {
print("Clear error: \(error)")
}
Error Handling
All errors are cases of the TSSessionError enum conforming to Error. Each error has a name property for machine-readable identification.
| Error | name | Condition |
|---|---|---|
notInitialized | notInitialized | SDK not initialized. Call initialize(config:) first |
deviceNotRegistered | deviceNotRegistered | Server-authoritative: the server reports the device is not registered. Call markDeviceRegistered() after registering with your backend |
sslPinningError(Error) | sslPinningError | TLS/SSL pinning validation failed — the server certificate did not match a pinned certificate |
challengeExpired | challengeExpired | Server challenge expired. Retry startCollect() |
invalidChallengeResponse | invalidChallengeResponse | Invalid or empty challenge from server |
notStarted | notStarted | No active collection session. Call startCollect() first |
bootstrapKeyNotConfigured | bootstrapKeyNotConfigured | Bootstrap RSA public key not configured |
keychainError(Error) | keychainError | Keychain read/write failed |
cryptoError(Error) | cryptoError | Cryptographic operation failed |
serializationError(Error) | serializationError | Failed to encode/decode a request or response payload |
networkError(Error) | networkError | Network request failed. The wrapped Error is an ApiError (see below) |
unknownError(Error) | unknownError | An unclassified error occurred |
For networkError(Error), the wrapped error is an ApiError. ApiError.server(statusCode:data:) carries the HTTP status code, and ApiError exposes serverErrorCode: String? and serverErrorMessage: String? — mirroring the server's errors[].code and errors[].message (see the API reference).
do {
try await TSBehavioralSDK.shared.startCollect()
} catch let error as TSSessionError {
switch error {
case .deviceNotRegistered:
// Server says the device is not registered — register via your backend first
print("Register device first")
case .sslPinningError(let underlyingError):
print("SSL pinning failed: \(underlyingError)")
case .networkError(let underlyingError):
if let apiError = underlyingError as? ApiError {
print("Network error [\(apiError.serverErrorCode ?? "-")]: \(apiError.serverErrorMessage ?? "")")
} else {
print("Network error: \(underlyingError)")
}
default:
print("SDK error [\(error.name)]: \(error.localizedDescription)")
}
}
Complete Example
A full view controller integrating all SDK functions: initialization, device registration, data collection, touch tracking, keyboard tracking, screen transitions, and finalization.
import UIKit
import BehaviorSDK
class BehavioralViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var submitButton: UIButton!
private var collectTask: Task<Void, Never>?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
collectTask = Task { @MainActor in
await setupAndStartCollection()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
collectTask?.cancel()
// Fast stop if user navigates away
try? TSBehavioralSDK.shared.stopCollect()
}
}
// MARK: - SDK Setup
func setupAndStartCollection() async {
do {
// 1. Configure SDK
let config = TSBehavioralConfig(
appId: "your-app-id",
baseUrl: "https://your-base-url/api",
metadataConfig: MetaDataCollectionConfig(
intervalMillis: 2000
),
sensorConfig: SensorConfig(
samplingIntervalMs: 20
),
keyboardConfig: KeyboardConfig(
trackRawText: false
),
touchConfig: TouchConfig(
enabled: true
)
)
// 2. Initialize SDK
let initInfo = try await TSBehavioralSDK.shared.initialize(
config: config
)
// 3. Register device (skip if already registered)
if !initInfo.isRegistered {
// Your backend calls:
// POST /v1/register_device
// Body: {
// "device_id": initInfo.deviceId,
// "key_pair_id": initInfo.bootstrapKeyId,
// "device_public_key": initInfo.encryptedDevicePublicKeyPayload,
// "encrypted_secret_key": initInfo.encryptedSecretKey,
// "cus_user_id": "your-user-id",
// "protocol": "encrypted"
// }
// Response: { "data": { "customer_user_device_id": "..." } }
let userDeviceId = try await yourBackend.registerDevice(
deviceId: initInfo.deviceId,
keyPairId: initInfo.bootstrapKeyId,
devicePublicKey: initInfo.encryptedDevicePublicKeyPayload,
encryptedSecretKey: initInfo.encryptedSecretKey,
cusUserId: "your-user-id"
)
try TSBehavioralSDK.shared.markDeviceRegistered(
isRegistered: true,
customerUserDeviceId: userDeviceId
)
}
// 4. Start data collection
try await TSBehavioralSDK.shared.startCollect()
// 5. Track screen transition (required, call immediately)
TSBehavioralSDK.shared.updateScreenTransition(
screenName: "login_screen"
)
// 6. Start keyboard tracking
TSBehavioralSDK.shared.startKeyboardTracking(
textField: emailTextField
)
TSBehavioralSDK.shared.startKeyboardTracking(
textField: passwordTextField
)
} catch let error as TSSessionError {
print("SDK error [\(error.name)]: \(error.localizedDescription)")
} catch {
print("Unexpected error: \(error)")
}
}
// MARK: - Screen Navigation
func navigateToPayment() {
// 7. Track screen transitions on navigation
TSBehavioralSDK.shared.updateScreenTransition(
screenName: "payment_screen"
)
// ... navigate to payment screen
}
// MARK: - Finalize
@IBAction func submitTapped(_ sender: UIButton) {
Task { @MainActor in
do {
// 8. Stop keyboard tracking
TSBehavioralSDK.shared.stopKeyboardTracking(
textField: emailTextField
)
TSBehavioralSDK.shared.stopKeyboardTracking(
textField: passwordTextField
)
// 9. Finalize — flushes data and drains upload queue
let result = try await TSBehavioralSDK.shared.finalizeCollect()
// Send challengeId to your backend to compute behavior score:
// POST /v1/behavior/score
// Body: { "challenge_id": result.challengeId }
try await yourBackend.computeBehaviorScore(
challengeId: result.challengeId
)
} catch let error as TSSessionError {
print("Finalize error [\(error.name)]: \(error.localizedDescription)")
}
}
}
// MARK: - Logout
func onLogout() {
// 10. Clear identity on logout
do {
try TSBehavioralSDK.shared.clearDeviceIdentity()
// Must call initialize() again before next use
} catch {
print("Clear error: \(error)")
}
}
}
Server-Side Device Registration
After calling initialize(config:), your app must register the device with TrustVision backend before calling startCollect(). This is a server-side call — typically done from your app via an authenticated API client.
API Endpoint
POST {baseUrl}/v1/register_device
Content-Type: application/json
Authorization: TV {accessKeyId}:{signature}
X-TV-Timestamp: {ISO8601 UTC timestamp}
Request Body
| Field | Type | Source | Description |
|---|---|---|---|
device_id | String | initInfo.deviceId | SDK-generated device identifier |
key_pair_id | String | initInfo.bootstrapKeyId | Bootstrap key pair identifier |
device_public_key | String | initInfo.encryptedDevicePublicKeyPayload | AES-GCM sealed device public key (base64) |
encrypted_secret_key | String | initInfo.encryptedSecretKey | RSA-OAEP encrypted AES secret key (base64) |
cus_user_id | String | Your app | Customer user identifier from your system |
protocol | String | — | Always "encrypted" |
{
"device_id": "from initInfo.deviceId",
"key_pair_id": "from initInfo.bootstrapKeyId",
"device_public_key": "from initInfo.encryptedDevicePublicKeyPayload",
"encrypted_secret_key": "from initInfo.encryptedSecretKey",
"cus_user_id": "your-user-identifier",
"protocol": "encrypted"
}
Response
{
"data": {
"customer_user_device_id": "server-assigned-id"
}
}
HMAC-SHA256 Authentication
All API requests require HMAC-SHA256 signature in the Authorization header.
String to sign:
{METHOD}\n{PATH}\n{TIMESTAMP}
METHOD— HTTP method (e.g.,POST)PATH— API path without base URL (e.g.,/v1/register_device)TIMESTAMP— ISO 8601 UTC timestamp (e.g.,2026-03-31T12:00:00.000+0000)
Signature: Base64(HMAC-SHA256(accessKeySecret, stringToSign))
Headers:
| Header | Value |
|---|---|
Authorization | TV {accessKeyId}:{signature} |
X-TV-Timestamp | ISO 8601 UTC timestamp |
Swift Example
import Foundation
import CryptoKit
struct DemoApiClient {
let endpoint: String
let apiKeyId: String
let apiSecretKey: String
// MARK: - Register Device
func registerDevice(
deviceId: String,
keyId: String,
encryptedDevicePublicKeyPayload: String,
encryptedSecretKey: String,
cusUserId: String
) async throws -> String {
let path = "/v1/register_device"
guard let url = URL(string: endpoint + path) else {
throw URLError(.badURL)
}
let body: [String: String] = [
"cus_user_id": cusUserId,
"device_id": deviceId,
"key_pair_id": keyId,
"device_public_key": encryptedDevicePublicKeyPayload,
"encrypted_secret_key": encryptedSecretKey,
"protocol": "encrypted",
]
let jsonData = try JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
applyAuthHeaders(to: &request, method: "POST", path: path)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let responseData = json?["data"] as? [String: Any]
return responseData?["customer_user_device_id"] as? String ?? ""
}
// MARK: - HMAC-SHA256 Signing
private func applyAuthHeaders(
to request: inout URLRequest,
method: String,
path: String
) {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let timestamp = formatter.string(from: Date())
let stringToSign = "\(method)\n\(path)\n\(timestamp)"
let hmac = HMAC<SHA256>.authenticationCode(
for: Data(stringToSign.utf8),
using: SymmetricKey(data: Data(apiSecretKey.utf8))
)
let signature = Data(hmac).base64EncodedString()
request.setValue(timestamp, forHTTPHeaderField: "X-TV-Timestamp")
request.setValue("TV \(apiKeyId):\(signature)", forHTTPHeaderField: "Authorization")
}
}
Full Registration Flow
// 1. Initialize SDK
let config = TSBehavioralConfig(
appId: "your-app-id",
baseUrl: "https://your-trustvision-endpoint.com/api"
)
let initInfo = try await TSBehavioralSDK.shared.initialize(config: config)
// 2. Register device via your backend
let client = DemoApiClient(
endpoint: "https://your-trustvision-endpoint.com/api",
apiKeyId: "your-access-key-id",
apiSecretKey: "your-access-key-secret"
)
let customerUserDeviceId = try await client.registerDevice(
deviceId: initInfo.deviceId,
keyId: initInfo.bootstrapKeyId,
encryptedDevicePublicKeyPayload: initInfo.encryptedDevicePublicKeyPayload,
encryptedSecretKey: initInfo.encryptedSecretKey,
cusUserId: "your-user-id"
)
// 3. Mark device as registered in SDK
try TSBehavioralSDK.shared.markDeviceRegistered(
isRegistered: true,
customerUserDeviceId: customerUserDeviceId
)
// 4. Now you can start collecting
try await TSBehavioralSDK.shared.startCollect()