iOS v1.0.0.3

Specifications

SpecValue
SDK Version1.0.3
Min iOS13.0
Swift5.5+ (async/await)
Xcode16.3
DistributionXCFramework
External DependencyNone (URLSession-based)

Installation

XCFramework

  1. Get the BehaviorSDK.xcframework from TrustVision
  2. Drag it into your Xcode project under Frameworks, Libraries, and Embedded Content
  3. Set Embed to Embed & Sign

Permissions (Info.plist)

Add the following keys if you enable Bluetooth or Location collection:

XML
<!-- 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

Swift
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

FieldTypeRequiredDefaultDescription
appIdStringYes—Your app's bundle identifier
baseUrlStringYes—Base URL for TrustVision API
metadataConfigMetaDataCollectionConfigNoDefault valuesDevice metadata collection settings
uploadBlockSizeIntNo10Number of batch entries before auto-upload
sensorConfigSensorConfigNoSensorConfig()Sensor data collection settings
keyboardConfigKeyboardConfigNoKeyboardConfig()Keyboard tracking settings
touchConfigTouchConfigNoTouchConfig()Touch tracking settings
screenTransitionBlockSizeIntNo1Screen transition batch size
appStateBlockSizeIntNo1Number of app-state events per upload batch
retryConfigRetryConfigNoRetryConfig()Upload retry queue settings
sslCertificates[TSCertificate]?NonilCertificates 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:

Swift
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

FieldTypeRequiredDefaultDescription
intervalMillisIntYes—Metadata collection interval in milliseconds
canAskPermissionBoolNofalseWhether SDK may request system permissions (Bluetooth, Location)
enabledBoolNotrueEnable/disable metadata collection
typeModes[MetaDataType: MetaDataCollectionMode]NodeviceDescription→manual, deviceBehaviors→interval, userBehaviors→intervalCollection mode per metadata type. .interval = repeating timer; .manual = explicit calls only
collectOnceTypesSet<MetaDataType>No[.deviceDescription]Types collected once at session start and stop (fire-and-forget boundary snapshots)

SensorConfig

FieldTypeRequiredDefaultDescription
samplingIntervalMsIntNo20 (50Hz)Sensor sampling interval in milliseconds. Must be > 0
enabledBoolNotrueEnable/disable sensor collection

KeyboardConfig

FieldTypeRequiredDefaultDescription
trackRawTextBoolNofalseWhether to collect raw typed text. When false, only character type is recorded
enabledBoolNotrueEnable/disable keyboard tracking
blockSizeIntNo1Number of keyboard events per upload batch (≥ 1)

TouchConfig

FieldTypeRequiredDefaultDescription
enabledBoolNotrueEnable/disable touch tracking

RetryConfig

FieldTypeRequiredDefaultDescription
enabledBoolNotrueEnable/disable the retry queue
maxRetriesIntNo2Max immediate retries per request (0–10). Total calls = value + 1
retryDelayMsIntNo1000Base delay before first retry in milliseconds (> 0)
maxDelayMsIntNo5000Maximum delay cap in milliseconds (≥ retryDelayMs)
maxRetryAttemptsIntNo3Maximum retry-queue attempts before dropping a failed batch (≥ 1)
maxRetryStorageItemsIntNo50Maximum items in retry storage before FIFO eviction (≥ 1)
finalFlushTimeoutSecondsTimeIntervalNo10.0Timeout in seconds for flush at session end (≥ 1)
retryItemDelaySecondsTimeIntervalNo0.5Delay 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:

Swift
func initialize(config: TSBehavioralConfig) async throws -> TSDeviceInitInfo

Parameters:

ParameterTypeRequiredDescription
configTSBehavioralConfigYesSDK configuration

Returns: TSDeviceInitInfo

FieldTypeDescription
deviceIdStringUnique device identifier (UUID)
bootstrapKeyIdStringKey pair ID for encryption
isRegisteredBoolWhether device was previously registered
encryptedDevicePublicKeyPayloadStringAES-GCM sealed device public key (base64). Pass to device_public_key in register API
encryptedSecretKeyStringRSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API

Throws: TSSessionError.keychainError, TSSessionError.cryptoError

Example:

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

Swift
func markDeviceRegistered(isRegistered: Bool, customerUserDeviceId: String) throws

Parameters:

ParameterTypeRequiredDescription
isRegisteredBoolYesRegistration status (true to mark as registered)
customerUserDeviceIdStringYesUnique user-device identifier returned by your server

Signature (Option B — raw server response):

Swift
func markDeviceRegistered(encryptedPayload: Data) throws
ParameterTypeRequiredDescription
encryptedPayloadDataYesRaw response data from your server's registration API. SDK extracts customer_user_device_id from the JSON

Throws: TSSessionError.keychainError

Example — using parsed values:

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

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

Swift
@MainActor
func startCollect() async throws

Must be called on @MainActor (main thread).

Throws: TSSessionError.deviceNotRegistered, TSSessionError.networkError, TSSessionError.invalidChallengeResponse

Example:

Swift
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 after startCollect() 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:

Swift
func updateScreenTransition(screenName: String)

Parameters:

ParameterTypeRequiredDescription
screenNameStringYesName of the screen being navigated to

Example — call immediately after startCollect:

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

Swift
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 = true are 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:

Swift
@MainActor func startKeyboardTracking(textField: UITextField)
@MainActor func startKeyboardTracking(textView: UITextView)

Parameters:

ParameterTypeRequiredDescription
textFieldUITextFieldYesText field to track
textViewUITextViewYesText view to track

Example:

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

Swift
func stopKeyboardTracking(textField: UITextField)
func stopKeyboardTracking(textView: UITextView)

Parameters:

ParameterTypeRequiredDescription
textFieldUITextFieldYesText field to stop tracking
textViewUITextViewYesText view to stop tracking

Example:

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

Swift
func addKeyboardTrackingData(_ data: TSTypingData)

Parameters:

ParameterTypeRequiredDescription
dataTSTypingDataYesKeyboard event data

TSTypingData fields:

FieldTypeRequiredDescription
timestampInt64YesEvent timestamp in milliseconds since epoch
textLengthInt?NoCurrent text field length
dataString?NoCharacter value (only if trackRawText is true)
typeTSKeyboardDataTypeYesType of keystroke
viewIdStringYesIdentifier for the input field

TSKeyboardDataType values: lowerText, upperText, number, spacing, specialCharacters, delete, cursorMoving, unknown, mixed

Example:

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

Swift
func trackTextChange(oldText: String, newText: String, viewId: String)

Parameters:

ParameterTypeRequiredDescription
oldTextStringYesText value before the change
newTextStringYesText value after the change
viewIdStringYesStable identifier for the input field

Example — SwiftUI (iOS 14–16):

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

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

Swift
func trackTouchEvent(event: TSPointerEventDto)

Parameters:

ParameterTypeRequiredDescription
eventTSPointerEventDtoYesTouch event data

TSPointerEventDto fields:

FieldTypeDescription
actionNameString?Action name: "down", "move", "up"
actionMaskedInt?Action constant
pointerIdInt?Pointer identifier
globalXDouble?Screen X coordinate
globalYDouble?Screen Y coordinate

Example:

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

Swift
@MainActor
func finalizeCollect() async throws -> TSFinalizeCollectResult

Must be called on @MainActor (main thread).

Returns: TSFinalizeCollectResult

FieldTypeDescription
challengeIdStringChallenge ID — pass to backend for scoring
nonceStringSession nonce
lastSeqIntLast uploaded batch sequence number
batchIdString?Last batch identifier (nil if no batches uploaded)
droppedBatchCountIntNumber of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize
lastUploadErrorString?Description of the last upload error that caused a drop, or nil when none

Observability (v1.0.0.3): droppedBatchCount and lastUploadError let you detect silent data loss after finalize. A non-zero droppedBatchCount means some collected batches never reached the server; inspect lastUploadError for the cause.

Throws: TSSessionError.notStarted

Example:

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

Swift
@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()
Asyncasync throwsthrows (sync)
Flush dataYesNo
Drain upload queueYes (waits)No
Data loss riskNonePossible
Use caseNormal completionApp backgrounding

Example:

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

Swift
func clearDeviceIdentity() throws

Throws: TSSessionError.keychainError

Example:

Swift
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.

ErrornameCondition
notInitializednotInitializedSDK not initialized. Call initialize(config:) first
deviceNotRegistereddeviceNotRegisteredServer-authoritative: the server reports the device is not registered. Call markDeviceRegistered() after registering with your backend
sslPinningError(Error)sslPinningErrorTLS/SSL pinning validation failed — the server certificate did not match a pinned certificate
challengeExpiredchallengeExpiredServer challenge expired. Retry startCollect()
invalidChallengeResponseinvalidChallengeResponseInvalid or empty challenge from server
notStartednotStartedNo active collection session. Call startCollect() first
bootstrapKeyNotConfiguredbootstrapKeyNotConfiguredBootstrap RSA public key not configured
keychainError(Error)keychainErrorKeychain read/write failed
cryptoError(Error)cryptoErrorCryptographic operation failed
serializationError(Error)serializationErrorFailed to encode/decode a request or response payload
networkError(Error)networkErrorNetwork request failed. The wrapped Error is an ApiError (see below)
unknownError(Error)unknownErrorAn 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).

Swift
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.

Swift
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

FieldTypeSourceDescription
device_idStringinitInfo.deviceIdSDK-generated device identifier
key_pair_idStringinitInfo.bootstrapKeyIdBootstrap key pair identifier
device_public_keyStringinitInfo.encryptedDevicePublicKeyPayloadAES-GCM sealed device public key (base64)
encrypted_secret_keyStringinitInfo.encryptedSecretKeyRSA-OAEP encrypted AES secret key (base64)
cus_user_idStringYour appCustomer user identifier from your system
protocolString—Always "encrypted"
JSON
{
  "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

JSON
{
  "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:

HeaderValue
AuthorizationTV {accessKeyId}:{signature}
X-TV-TimestampISO 8601 UTC timestamp

Swift Example

Swift
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

Swift
// 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()