> For the complete documentation index, see [llms.txt](https://docs.mediarithmics.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.mediarithmics.io/setup-and-monitor/collect-and-manage-your-data/collect-data-in-real-time/app-tracking.md).

# App tracking

Track mobile and CTV applications through the tracking API, with signature authentication.

## Your goal

Send activities from your mobile or CTV application to the platform, authenticated in a way that is safe for code running on a user's device.

## Expected outcome

App activities arriving with the right activity type, the right channel ID, and correctly formatted device identifiers - and no long-term token embedded in the application.

## What app tracking requires

Four things, and none of them are optional:

| Requirement                              | Detail                                                                                                                                                                                                                         |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **The tracking API**                     | All user activities are sent using the [tracking API](/setup-and-monitor/collect-and-manage-your-data/collect-data-in-real-time/server-side-events.md) - there is no app SDK to install.                                       |
| **Signature authentication**             | Requests between your application and mediarithmics are authenticated with signature authentication. See [Manage API access](/setup-and-monitor/manage-the-platform/manage-api-access.md#signature-authentication).            |
| **Correct payload**                      | The activity and events must follow the specifics below.                                                                                                                                                                       |
| **Correctly formatted `$user_agent_id`** | Device identifiers use the formats described in [User Device](/setup-and-monitor/collect-and-manage-your-data/design-your-data-model-and-schema/user-identity/user-identifiers/user-device.md#mobile-application-identifiers). |

{% hint style="danger" %}
Do not embed a long-term API token in an application. It can leak by decompiling the application - which is exactly why signature authentication exists. Contact your account manager for details on setting it up.
{% endhint %}

## Two ways to integrate

{% columns %}
{% column %}

### Call the API from the app

Integrate a small piece of code - around 100 lines - into the application to make the calls to the tracking API. Complete [samples](#sample-integration) for iOS, Android, Tizen and webOS, Android TV and Roku are below.
{% endcolumn %}

{% column %}

### Forward from an existing analytics tool

Reuse an analytics tool you already have in the app, and transfer events from that solution's **server** to the mediarithmics API.
{% endcolumn %}
{% endcolumns %}

The second option keeps the credential on your servers rather than in the app, at the cost of depending on the other tool's export.

## Activity formatting

{% tabs %}
{% tab title="Mobile applications" %}

| Field             | Type             | Description                                                                          |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------ |
| `$type`           | String enum      | Must be `APP_VISIT`.                                                                 |
| `$session_status` | String enum      | See the session status guidance below.                                               |
| `$user_agent_id`  | String, optional | The user agent identifier of the device, in the unified representation.              |
| `$app_id`         | String           | The mobile app ID - the channel, created beforehand in Navigator or through the API. |
| {% endtab %}      |                  |                                                                                      |

{% tab title="CTV applications" %}

| Field             | Type             | Description                                                                       |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
| `$type`           | String enum      | Must be `CTV_VISIT`.                                                              |
| `$session_status` | String enum      | See the session status guidance below.                                            |
| `$user_agent_id`  | String, optional | The user agent identifier of the device, in the unified representation.           |
| `$ctv_id`         | String           | The CTV app ID - the channel, created beforehand in Navigator or through the API. |

{% hint style="info" %}
For CTV application tracking, automatic capture of IP addresses is available. See [Network IDs](/setup-and-monitor/collect-and-manage-your-data/design-your-data-model-and-schema/user-identity/network-ids.md).
{% endhint %}
{% endtab %}
{% endtabs %}

### Choosing a session status

This is the decision that shapes your integration:

| Value            | When to use it                                                  | Consequence                                                                                                                         |
| ---------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `IN_SESSION`     | **Recommended.** One API call per tracked event.                | The platform automatically aggregates all the events of a session sent across many calls.                                           |
| `CLOSED_SESSION` | Only if you make **a single API call per session**, at its end. | You must provide **all** the events of the session in the `$events` array. Each call with this value generates a new user activity. |

## Predefined app events

Predefined event names are available out of the box to simplify and automate event processing:

| Event          | Meaning                                                 | Who sends it                                                                                                              |
| -------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `$app_open`    | The app was opened, or resumed and became active again. | **You.**                                                                                                                  |
| `$app_install` | The app was installed.                                  | **The platform.** Triggered the first time an app open event is received for a user, whether the user is new or existing. |
| `$app_update`  | The app was updated.                                    | **The platform.** Triggered when the SDK version, app version, or OS version changes from one open to the next.           |

{% hint style="info" %}
You do not send `$app_install` or `$app_update` - they are calculated server-side from `$app_open`. Sending them yourself would double-count.
{% endhint %}

The full catalogue is in [Standard events](/setup-and-monitor/collect-and-manage-your-data/design-your-data-model-and-schema/standard-events.md).

## Sample integration

Each sample below is a complete integration for one platform: it signs the payload with `HMAC-SHA256`, builds an activity of the shape described above, and posts it to the tracking API. The `signPayload` function every sample opens with is the digest specification documented on [Manage API access](/setup-and-monitor/manage-the-platform/manage-api-access.md#sign-a-call).

Replace the placeholder constants with your own values - the datamart ID, the channel ID that goes in `$app_id` or `$ctv_id`, the compartment and registry IDs, and the secret key with its `key_id`.

{% hint style="info" %}
The samples hold `SECRET_KEY_VALUE` as a constant so that they stay readable. It is the value returned once, when the key is created - see [Generate a secret key](/setup-and-monitor/manage-the-platform/manage-api-access.md#generate-a-secret-key).
{% endhint %}

{% tabs %}
{% tab title="Mobile applications" %}

<details>

<summary>iOS - Swift</summary>

```swift
import Foundation
import CryptoKit

func signPayload(uri:String, secretKeyId:String, timestamp:Int64, payload:String, secretKeyValue:String) -> String {
    let message = Data("\(uri)\n\(secretKeyId)\n\(String(timestamp))\n\(payload)".utf8)
    let symmetricKey = SymmetricKey(data: Data(secretKeyValue.utf8))
    let hmac = HMAC<SHA256>.authenticationCode(for: message, using: symmetricKey)
    let base64hmac = Data(hmac).base64EncodedString()
    return base64hmac
}

func sendUserEvent(
    userAccountId:String?,
    mobileVendorId:String?,
    mobileAdId:String?,
    eventName:String,
    category:String?,
    subCategory:String?) {

        // Constant parameters that could be defined in a configuration file
        let DATAMART_ID="XXX"           // the id of the database
        let APPLICATION_ID="XXX"        // the id of the mobile application
        let COMPARTMENT_ID="XXX"        // the id used to define the nature of the user account id
        let VENDOR_ID_REGISTRY_ID="XXX" // the id used to define the nature of the mobile vendor id
        let SECRET_KEY_ID="XXX" // the id of the secret key used to sign the message
        let SECRET_KEY_VALUE="ZZZZZZZZZZZZZZZZZZZZZZZZZZ" // the secret key value

        // The URL entry is static
        let uri = "/v1/datamarts/"+DATAMART_ID+"/user_activities"
        let url = "https://api.mediarithmics.com" + uri
        let timestamp = Int64(Date().timeIntervalSince1970 * 1000)

        // Prepare the json payload
        var identifiers:[[String:Any]]  = []
        if let getUserAccountId = userAccountId  { identifiers.append(["$type":"USER_ACCOUNT", "$compartment_id":COMPARTMENT_ID, "$user_account_id": getUserAccountId ]) }
        if let getMobileVendorId = mobileVendorId  { identifiers.append(["$type":"USER_AGENT", "$user_agent_id": ("mov:ios:" + VENDOR_ID_REGISTRY_ID + ":" + getMobileVendorId) ]) }
        if let getMobileAdId = mobileAdId  { identifiers.append(["$type":"USER_AGENT", "$user_agent_id": ("mob:ios:raw:" + getMobileAdId) ]) }

        let jsonPayload: [String: Any] = [
            "$type": "APP_VISIT",
            "$app_id": APPLICATION_ID,
            "$session_status": "IN_SESSION",
            "$ts" : timestamp,
            "$user_identifiers" : identifiers,
            "$user_agent_info" : [
                "$initial_ts": 1759742895725,
                "$form_factor": "SMARTPHONE",
                "$os_family": "IOS",
                "$browser_family": "CHROME",
                "$browser_version": "141.0.0.0",
                "$brand": "Apple",
                "$model": "Iphone 14",
                "$os_version": "IOS26",
                "$carrier": "AT&T",
                "$raw_value": nil,
                "$agent_type": "MOBILE_APP"
                ],
            "$events":[[
                "$event_name" : eventName,
                "$ts" : timestamp,
                "$properties" : [
                    "category" : category ?? nil,
                    "sub_category" : subCategory ?? nil ]]]]

        // Create a URLRequest with the specified URL
        var request = URLRequest(url: URL(string:url)!)

        do {
            // Set the HTTP method
            request.httpMethod = "POST"

            // Convert the dictionary to JSON data
            let jsonData = try JSONSerialization.data(withJSONObject: jsonPayload, options: [])
            request.httpBody = jsonData
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")

            // Build the HMAC signature
            let base64hmac = signPayload(uri:uri, secretKeyId: SECRET_KEY_ID, timestamp: timestamp, payload: String(data:jsonData, encoding: .utf8)!, secretKeyValue: SECRET_KEY_VALUE)

            // Set the specific headers
            request.setValue(SECRET_KEY_ID, forHTTPHeaderField: "X-Mics-Key-Id")
            request.setValue(String(timestamp), forHTTPHeaderField: "X-Mics-Ts")
            request.setValue(base64hmac, forHTTPHeaderField: "X-Mics-Mac")

        } catch {
            print("Error while building the request data: \(error)")
            return
        }

        // Create a URLSession task to perform the API call
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            // Check for errors
            if let error = error {
                print("Error: \(error)")
                return
            }

            // Check for a valid HTTP response
            guard let httpResponse = response as? HTTPURLResponse else {
                print("Invalid HTTP response")
                return
            }

            // Check for a successful status code (e.g., 200 OK)
            if httpResponse.statusCode == 200 {
                let responseDataString = data.flatMap { String(data: $0, encoding: .utf8) } ?? "No response data"
                print("HTTP Response OK!, body:\(responseDataString) ")
            } else {
                let responseDataString = data.flatMap { String(data: $0, encoding: .utf8) } ?? "No response data"
                print("HTTP Response Error: \(httpResponse.statusCode)\n\(responseDataString)")
                return
            }
        }

        // Start the URLSession task
        task.resume()
}


```

</details>

<details>

<summary>Android - Kotlin</summary>

```kotlin
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec


fun signPayload(uri: String, secretKeyId: String, timestamp: Long, payload: String, secretKeyValue: String): String {
    val message = "$uri\n$secretKeyId\n$timestamp\n$payload".toByteArray(Charsets.UTF_8)
    val secretKey = SecretKeySpec(secretKeyValue.toByteArray(Charsets.UTF_8), "HmacSHA256")
    val hmac = Mac.getInstance("HmacSHA256").apply { init(secretKey) }.doFinal(message)
    return Base64.getEncoder().encodeToString(hmac)
}

fun sendUserEvent(
    userAccountId: String?,
    userVendorId: String?,
    userMobileAdId: String?,
    eventName: String,
    category: String?,
    subCategory: String?
) {

    // Constant parameters that could be defined in a configuration file
    val DATAMART_ID = "XXX"          // the id of the database
    val APPLICATION_ID = "XXX"       // the id of the mobile application
    val COMPARTMENT_ID = "XXX"       // the id used to define the nature of the user account id
    val SECRET_KEY_ID = "XXXX" // the id of the secret key used to sign the message
    val SECRET_KEY_VALUE = "ZZZZZZZZ" // the secret key value

    // The URL entry is static
    val uri = "/v1/datamarts/$DATAMART_ID/user_activities"
    val url = "https://api.mediarithmics.com$uri"
    val timestamp = System.currentTimeMillis()

    // Prepare the JSON payload
    val identifiers = mutableListOf<String>()
    userAccountId?.let { it -> identifiers.add(""" { "${'$'}type" : "USER_ACCOUNT" , "${'$'}compartment_id" : "$COMPARTMENT_ID", "${'$'}user_account_id" : "$it" }""") }

    val jsonPayload = """{
            "${'$'}type" : "APP_VISIT",
            "${'$'}app_id" : "$APPLICATION_ID",
            "${'$'}session_status" : "IN_SESSION",
            "${'$'}ts" : $timestamp,
            "${'$'}user_identifiers" : ${identifiers.toList().joinToString(",", "[", "]")},
            "${'$'}user_agent_info" : {
                "${'$'}initial_ts": 1759742895725,
                "${'$'}form_factor": "SMARTPHONE",
                "${'$'}os_family": "ANDROID",
                "${'$'}browser_family": "CHROME",
                "${'$'}browser_version": "141.0.0.0",
                "${'$'}brand": "SAMSUNG",
                "${'$'}model": "GALAXY S20",
                "${'$'}os_version": "ANDROID 15",
                "${'$'}carrier": "AT&T",
                "${'$'}raw_value": null,
                "${'$'}agent_type": "MOBILE_APP"
            },
            "${'$'}events" : [
                    {
                            "${'$'}event_name" : "$eventName",
                            "${'$'}ts" : $timestamp,
                            "${'$'}properties" : {
                                    "category" : ${if (category == null) "null" else "\"${category}\""},
                                    "sub_category" : ${if (subCategory == null) "null" else "\"${subCategory}\""}
                            }
                    }
            ]
    }"""

    // Create a HttpURLConnection with the specified URL
    val urlConnection = URL(url).openConnection() as HttpURLConnection

    try {
        // Set the HTTP method
        urlConnection.requestMethod = "POST"

        // Enable output streams
        urlConnection.doOutput = true
        urlConnection.doInput = true

        // Convert the map to JSON data
        val jsonData = jsonPayload.toString().toByteArray(Charsets.UTF_8)

        urlConnection.setRequestProperty("charset", "utf-8")
        urlConnection.setRequestProperty("Content-Type", "application/json")
        urlConnection.setRequestProperty("Content-length", jsonData.size.toString())

        println(jsonData.toString(Charsets.UTF_8))
        // Build the HMAC signature
        val base64hmac = signPayload(uri, SECRET_KEY_ID, timestamp, String(jsonData, Charsets.UTF_8), SECRET_KEY_VALUE)

        // Set the specific headers
        urlConnection.setRequestProperty("X-Mics-Key-Id", SECRET_KEY_ID)
        urlConnection.setRequestProperty("X-Mics-Ts", timestamp.toString())
        urlConnection.setRequestProperty("X-Mics-Mac", base64hmac)

        //urlConnection.disconnect()

        val outputStream: DataOutputStream = DataOutputStream(urlConnection.outputStream)
        outputStream.write(jsonData)
        outputStream.flush()

    } catch (e: Exception) {
        println("Error while building the request data: $e")
        return
    }

    // Perform the API call
    try {
        val responseCode = urlConnection.responseCode
        // Check for a successful status code (e.g., 200 OK)
        if (responseCode == 200) {
            val responseDataString = urlConnection.inputStream.bufferedReader().use { it.readText() }
            println("HTTP Response OK!, body: $responseDataString")
        } else {
            val responseDataString = urlConnection.errorStream.bufferedReader().use { it.readText() }
            println("HTTP Response Error: $responseCode\n$responseDataString")
        }
    } catch (e: Exception) {
        println("Error: $e")
    } finally {
        // Disconnect after the request is complete
        urlConnection.disconnect()
    }
}
```

</details>
{% endtab %}

{% tab title="CTV applications" %}

<details>

<summary>Tizen and webOS - JavaScript</summary>

```javascript
/**
 * Mediarithmics User Activity Tracker - Tizen (Samsung Smart TV) and webOS (LG Smart TV)
 *
 * Dependencies:
 *   - CryptoJS (HMAC-SHA256 + Base64): include via
 *     <script src="libs/crypto-js.min.js"></script>
 *     Download from: https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
 *
 * Tested on: Tizen/webOS 4.0+
 */

// ============================================================
// Configuration - adapt these values to your environment
// ============================================================
var CONFIG = {
    DATAMART_ID: "XXX",
    APPLICATION_ID: "XXX",
    COMPARTMENT_ID: "XXX",
    REGISTRY_ID: "XXX",
    SECRET_KEY_ID: "XXX",
    SECRET_KEY_VALUE: "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
    API_BASE_URL: "https://api.mediarithmics.com"
};

// ============================================================
// HMAC-SHA256 Signature (using CryptoJS - no native dependency)
// ============================================================
function signPayload(uri, secretKeyId, timestamp, payload, secretKeyValue) {
    var message = uri + "\n" + secretKeyId + "\n" + timestamp + "\n" + payload;
    var hash = CryptoJS.HmacSHA256(message, secretKeyValue);
    return CryptoJS.enc.Base64.stringify(hash);
}

// ============================================================
// Send User Event
// ============================================================
function sendUserEvent(userAccountId, deviceId, eventName, category, subCategory) {

    var uri = "/v1/datamarts/" + CONFIG.DATAMART_ID + "/user_activities";
    var url = CONFIG.API_BASE_URL + uri;
    var timestamp = Date.now();

    // Build user identifiers
    var identifiers = [];
    if (userAccountId) {
        identifiers.push({
            "$type": "USER_ACCOUNT",
            "$compartment_id": CONFIG.COMPARTMENT_ID,
            "$user_account_id": userAccountId
        });
    }
    if (deviceId) {
        identifiers.push({
            "$type": "USER_AGENT",
            "$user_agent_id": "tv:" + CONFIG.REGISTRY_ID + ":" + deviceId
        });
    }

    // Build JSON payload
    var body = {
        "$type": "CTV_VISIT",
        "$ctv_id": CONFIG.APPLICATION_ID,
        "$session_status": "IN_SESSION",
        "$ts": timestamp,
        "$user_identifiers": identifiers,
        "$user_agent_info": {
            "$initial_ts": 1759742895725,
            "$form_factor": "SMART_TV",
            "$os_family": "TIZEN", // "WEB_OS"
            "$browser_family": null,
            "$browser_version": null,
            "$brand": "SAMSUNG", // "LG"
            "$model": "UE55CU7170",//"OLED55C3PSA"
            "$os_version": "TIZEN 7.0", //"WEBOS 23"
            "$carrier": null,
            "$raw_value": null,
            "$agent_type": "CTV_APP"
        },
        "$events": [{
            "$event_name": eventName,
            "$ts": timestamp,
            "$properties": {
                "category": category || null,
                "sub_category": subCategory || null
            }
        }]
    };

    var jsonString = JSON.stringify(body);

    // Sign the payload
    var base64hmac = signPayload(uri, CONFIG.SECRET_KEY_ID, timestamp, jsonString, CONFIG.SECRET_KEY_VALUE);

    // Send HTTP POST
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, true);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.setRequestHeader("X-Mics-Key-Id", CONFIG.SECRET_KEY_ID);
    xhr.setRequestHeader("X-Mics-Ts", String(timestamp));
    xhr.setRequestHeader("X-Mics-Mac", base64hmac);

    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                console.log("HTTP Response OK! body: " + xhr.responseText);
            } else {
                console.error("HTTP Response Error: " + xhr.status + "\n" + xhr.responseText);
            }
        }
    };

    xhr.onerror = function () {
        console.error("Network error during API call");
    };

    xhr.send(jsonString);
}

// ============================================================
// Usage example
// ============================================================
// sendUserEvent("user123", "tizen-device-abc", "page_view", "homepage", null);
```

</details>

<details>

<summary>Android TV and Google TV - Kotlin</summary>

```kotlin
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

/**
 * Mediarithmics User Activity Tracker - Android TV / Google TV
 *
 * Dependencies: None (standard Java/Kotlin APIs)
 * Compatible with: Android TV 10+, Google TV
 *
 * IMPORTANT: Call sendUserEvent() from a background thread or coroutine,
 * NOT from the main/UI thread.
 */

// ============================================================
// Configuration - adapt these values to your environment
// ============================================================
object MicsConfig {
    const val DATAMART_ID = "XXX"
    const val APPLICATION_ID = "XXX"
    const val COMPARTMENT_ID = "XXX"
    const val REGISTRY_ID = "XXX"
    const val SECRET_KEY_ID = "XXX"
    const val SECRET_KEY_VALUE = "ZZZZZZZZZZZZZZZZZZZZZZZZZZ"
    const val API_BASE_URL = "https://api.mediarithmics.com"
}

// ============================================================
// HMAC-SHA256 Signature
// ============================================================
fun signPayload(uri: String, secretKeyId: String, timestamp: Long, payload: String, secretKeyValue: String): String {
    val message = "$uri\n$secretKeyId\n$timestamp\n$payload".toByteArray(Charsets.UTF_8)
    val secretKey = SecretKeySpec(secretKeyValue.toByteArray(Charsets.UTF_8), "HmacSHA256")
    val hmac = Mac.getInstance("HmacSHA256").apply { init(secretKey) }.doFinal(message)
    return Base64.getEncoder().encodeToString(hmac)
}

// ============================================================
// Send User Event
// ============================================================
fun sendUserEvent(
    userAccountId: String?,
    deviceId: String?,
    eventName: String,
    category: String?,
    subCategory: String?
) {
    val uri = "/v1/datamarts/${MicsConfig.DATAMART_ID}/user_activities"
    val url = "${MicsConfig.API_BASE_URL}$uri"
    val timestamp = System.currentTimeMillis()

    // Build user identifiers
    val identifiers = mutableListOf<String>()
    userAccountId?.let {
        identifiers.add("""{"${'$'}type":"USER_ACCOUNT","${'$'}compartment_id":"${MicsConfig.COMPARTMENT_ID}","${'$'}user_account_id":"$it"}""")
    }
    deviceId?.let {
        identifiers.add("""{"${'$'}type":"USER_AGENT","${'$'}user_agent_id":"tv:${MicsConfig.REGISTRY_ID}:$it"}""")
    }

    // Build JSON payload
    val jsonPayload = """{
        "${'$'}type": "CTV_VISIT",
        "${'$'}ctv_id": "${MicsConfig.APPLICATION_ID}",
        "${'$'}session_status": "IN_SESSION",
        "${'$'}ts": $timestamp,
        "${'$'}user_identifiers": [${identifiers.joinToString(",")}],
        "${'$'}user_agent_info": {
            "${'$'}initial_ts": 1759742895725,
            "${'$'}form_factor": "SMART_TV",
            "${'$'}os_family": "ANDROID_TV",
            "${'$'}browser_family": null,
            "${'$'}browser_version": null,
            "${'$'}brand": "TCL",
            "${'$'}model": "TCL 55S546",
            "${'$'}os_version": "ANDROID_TV 14",
            "${'$'}carrier": null,
            "${'$'}raw_value": null,
            "${'$'}agent_type": "CTV_APP"
        },
        "${'$'}events": [{
            "${'$'}event_name": "$eventName",
            "${'$'}ts": $timestamp,
            "${'$'}properties": {
                "category": ${if (category == null) "null" else "\"$category\""},
                "sub_category": ${if (subCategory == null) "null" else "\"$subCategory\""}
            }
        }]
    }"""

    val jsonData = jsonPayload.toByteArray(Charsets.UTF_8)

    // Sign the payload
    val base64hmac = signPayload(uri, MicsConfig.SECRET_KEY_ID, timestamp, String(jsonData, Charsets.UTF_8), MicsConfig.SECRET_KEY_VALUE)

    // Send HTTP request
    val urlConnection = URL(url).openConnection() as HttpURLConnection
    try {
        urlConnection.requestMethod = "POST"
        urlConnection.doOutput = true
        urlConnection.doInput = true
        urlConnection.setRequestProperty("charset", "utf-8")
        urlConnection.setRequestProperty("Content-Type", "application/json")
        urlConnection.setRequestProperty("Content-length", jsonData.size.toString())
        urlConnection.setRequestProperty("X-Mics-Key-Id", MicsConfig.SECRET_KEY_ID)
        urlConnection.setRequestProperty("X-Mics-Ts", timestamp.toString())
        urlConnection.setRequestProperty("X-Mics-Mac", base64hmac)

        DataOutputStream(urlConnection.outputStream).use { os ->
            os.write(jsonData)
            os.flush()
        }

        val responseCode = urlConnection.responseCode
        if (responseCode == 200) {
            val responseBody = urlConnection.inputStream.bufferedReader().use { it.readText() }
            println("HTTP Response OK! body: $responseBody")
        } else {
            val errorBody = urlConnection.errorStream.bufferedReader().use { it.readText() }
            println("HTTP Response Error: $responseCode\n$errorBody")
        }

    } catch (e: Exception) {
        println("Error during API call: $e")
    } finally {
        urlConnection.disconnect()
    }
}

// ============================================================
// Usage example (call from a background thread!)
// ============================================================
// Thread { sendUserEvent("user123", "androidtv-device-abc", "page_view", "homepage", null) }.start()
```

</details>

<details>

<summary>Roku - BrightScript</summary>

```js
' ============================================================
' Mediarithmics User Activity Tracker - Roku (BrightScript)
'
' Dependencies: None (native Roku APIs)
' Requires: Roku OS 9.1+ (for roHMAC + FormatJSON)
' ============================================================

' ============================================================
' Configuration - adapt these values to your environment
' ============================================================
function getMicsConfig() as Object
    return {
        DATAMART_ID: "XXX",
        APPLICATION_ID: "XXX",
        COMPARTMENT_ID: "XXX",
        REGISTRY_ID: "XXX",
        SECRET_KEY_ID: "XXX",
        SECRET_KEY_VALUE: "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
        API_BASE_URL: "https://api.mediarithmics.com"
    }
end function

' ============================================================
' HMAC-SHA256 Signature
' ============================================================
function signPayload(uri as String, secretKeyId as String, timestampStr as String, payload as String, secretKeyValue as String) as String
    message = uri + Chr(10) + secretKeyId + Chr(10) + timestampStr + Chr(10) + payload

    hmac = CreateObject("roHMAC")
    keyBytes = CreateObject("roByteArray")
    keyBytes.FromAsciiString(secretKeyValue)
    hmac.Setup("SHA256", keyBytes)

    messageBytes = CreateObject("roByteArray")
    messageBytes.FromAsciiString(message)

    signatureBytes = hmac.Process(messageBytes)
    return signatureBytes.ToBase64String()
end function

' ============================================================
' Get timestamp in milliseconds as string
' ============================================================
function getTimestampMs() as String
    dateObj = CreateObject("roDateTime")
    seconds = dateObj.AsSeconds()
    return seconds.ToStr() + "000"
end function

' ============================================================
' Send User Event
' ============================================================
sub sendUserEvent(userAccountId as String, deviceId as String, eventName as String, category as String, subCategory as String)
    config = getMicsConfig()

    uri = "/v1/datamarts/" + config.DATAMART_ID + "/user_activities"
    url = config.API_BASE_URL + uri

    timestampStr = getTimestampMs()
    timestampInt = timestampStr.ToInt()

    ' Build user identifiers
    identifiers = CreateObject("roArray", 2, true)

    if userAccountId <> "" and userAccountId <> invalid
        id = CreateObject("roAssociativeArray")
        id["$type"] = "USER_ACCOUNT"
        id["$compartment_id"] = config.COMPARTMENT_ID
        id["$user_account_id"] = userAccountId
        identifiers.Push(id)
    end if

    if deviceId <> "" and deviceId <> invalid
        id = CreateObject("roAssociativeArray")
        id["$type"] = "USER_AGENT"
        id["$user_agent_id"] = "tv:" + config.REGISTRY_ID + ":" + deviceId
        identifiers.Push(id)
    end if

    ' Build event properties
    props = CreateObject("roAssociativeArray")
    if category <> "" and category <> invalid
        props["category"] = category
    else
        props["category"] = invalid
    end if
    if subCategory <> "" and subCategory <> invalid
        props["sub_category"] = subCategory
    else
        props["sub_category"] = invalid
    end if

    ' Build event
    evt = CreateObject("roAssociativeArray")
    evt["$event_name"] = eventName
    evt["$ts"] = timestampInt
    evt["$properties"] = props

    events = CreateObject("roArray", 1, true)
    events.Push(evt)

    ' Build user agent info
    agentInfo = CreateObject("roAssociativeArray")
    agentInfo["$initial_ts"] = 1759742895725#
    agentInfo["$form_factor"] = "SMART_TV"
    agentInfo["$os_family"] = "ROKU_TV"
    agentInfo["$browser_family"] = invalid
    agentInfo["$browser_version"] = invalid
    agentInfo["$brand"] = "HISENSE"
    agentInfo["$model"] = "Hisense 50R6E3"
    agentInfo["$os_version"] = "ROKU_TV 14.1"
    agentInfo["$carrier"] = invalid
    agentInfo["$raw_value"] = invalid
    agentInfo["$agent_type"] = "CTV_APP"

    ' Build full payload
    body = CreateObject("roAssociativeArray")
    body["$type"] = "CTV_VISIT"
    body["$ctv_id"] = config.APPLICATION_ID
    body["$session_status"] = "IN_SESSION"
    body["$ts"] = timestampInt
    body["$user_identifiers"] = identifiers
    body["$user_agent_info"] = agentInfo
    body["$events"] = events

    ' Serialize to JSON using native FormatJSON
    jsonPayload = FormatJSON(body)

    ' Sign the payload
    base64hmac = signPayload(uri, config.SECRET_KEY_ID, timestampStr, jsonPayload, config.SECRET_KEY_VALUE)

    ' Send HTTP POST request
    httpTransfer = CreateObject("roUrlTransfer")
    httpTransfer.SetUrl(url)
    httpTransfer.SetCertificatesFile("common:/certs/ca-bundle.crt")
    httpTransfer.InitClientCertificates()
    httpTransfer.AddHeader("Content-Type", "application/json")
    httpTransfer.AddHeader("X-Mics-Key-Id", config.SECRET_KEY_ID)
    httpTransfer.AddHeader("X-Mics-Ts", timestampStr)
    httpTransfer.AddHeader("X-Mics-Mac", base64hmac)

    responseCode = httpTransfer.PostFromString(jsonPayload)

    if responseCode = 200
        print "HTTP Response OK!"
    else
        print "HTTP Response Error: " ; responseCode
    end if
end sub

' ============================================================
' Usage example
' ============================================================
' sendUserEvent("user123", "roku-device-abc", "page_view", "homepage", "")
```

</details>
{% endtab %}
{% endtabs %}

## Verify

* The `API` step shows events in ingestion monitoring. See [Monitor data ingestion](/setup-and-monitor/operate-and-monitor/monitor-data-ingestion.md).
* Activities appear with type `APP_VISIT` or `CTV_VISIT` and the expected channel ID.
* Device identifiers resolve to a device point rather than being dropped, which means the registry exists.
* `$app_install` appears once for a new user, without you sending it.

## Troubleshooting

| Symptom                                     | Likely cause                                                                                                               |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Calls are rejected                          | The signature is computed over the wrong message. Check the order `uri`, `key_id`, `timestamp`, `body`, separated by `\n`. |
| One activity per event instead of a session | `$session_status` is `CLOSED_SESSION` - each such call creates a new activity.                                             |
| Device identifiers do not appear            | The `$user_agent_id` format is wrong for the registry type, or the registry does not exist.                                |
| Events land on the wrong property           | `$app_id` or `$ctv_id` points at the wrong channel.                                                                        |

## Related pages

<table data-view="cards" data-search="false"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Manage API access</strong></td><td>Signature authentication, keys, and the digest.</td><td><a href="/setup-and-monitor/manage-the-platform/manage-api-access.md">Manage API access</a></td></tr><tr><td><strong>Server-side events</strong></td><td>The tracking API this integration uses.</td><td><a href="/setup-and-monitor/collect-and-manage-your-data/collect-data-in-real-time/server-side-events.md">Server-side events</a></td></tr><tr><td><strong>User Device</strong></td><td>Mobile and CTV identifier formats.</td><td><a href="/setup-and-monitor/collect-and-manage-your-data/design-your-data-model-and-schema/user-identity/user-identifiers/user-device.md">User Device</a></td></tr></tbody></table>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.mediarithmics.io/setup-and-monitor/collect-and-manage-your-data/collect-data-in-real-time/app-tracking.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
