# Authentication

## Choosing the authentication method

There are two ways to authenticate to the mediarithmics API:

1. [Long-term API tokens](#long-term-api-tokens)
2. [Signature authentication](#signature-authentication)

{% hint style="danger" %} <mark style="color:red;">**Important:**</mark> <mark style="color:red;"></mark><mark style="color:red;">Please carefully choose the authentication method based on your use case. Choosing the wrong method can lead to significant security risks (access violation, data leakage, etc.)</mark>&#x20;
{% endhint %}

| Use case                               | Secure authentication method                          |
| -------------------------------------- | ----------------------------------------------------- |
| Direct servers integration             | [Long-term API tokens](#long-term-api-tokens)         |
| Live data collection from applications | [Signature authentication](#signature-authentication) |

Please ensure to use the right secure authentication method to avoid any security risks like access violation, data leakage, etc. For example, if you use the long-term API token into your mobile application, it could leak by decompiling the application.

{% hint style="info" %}
Get in touch with your Account manager if you want further information on the best authentication method to pick for your needs.
{% endhint %}

## Long-term API tokens

Long-term API tokens are the way to go if you plan to make **server-to-server calls**.

{% hint style="info" %}
We recommend creating service accounts to bear long-term API tokens, in order to avoid cancelling your token along with deleting a regular user.
{% endhint %}

A long-term API token identifies a user but it has a **longer expiration delay** (usually from **12 to 24 months**).

A user can have multiple long-term tokens, which can be useful to manage tokens renewal: just create a new API token before the old one expires and update your app.

There are 2 ways you can create a long-term API token:

* In the Navigator application
* Using the API

Once generated you can use the API token in the `Authorization` header of all your requests.

### List and create tokens in the UI

1. Head to Navigator
2. Navigate to settings
3. Then My Account > API Tokens
4. Click on the "New API Token" to generate a long-term API token

![](https://4196284719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MMuoqM-5hJ5JY0WnAKL%2F-MPA-tfz9RyKH_lBxGmw%2F-MPAJ2BFb1KFN-gvWEYi%2Fimage.png?alt=media\&token=5bfa7cb8-de61-4f71-9374-6228e5bddefc)

### Generating using the API

<mark style="color:green;">`POST`</mark> `https://api.mediarithmics.com/v1/users/:userId/api_tokens`

Generates an API token for a mediarithmics user

#### Path Parameters

| Name     | Type    | Description        |
| -------- | ------- | ------------------ |
| user\_id | integer | The ID of the user |

#### Headers

| Name          | Type   | Description              |
| ------------- | ------ | ------------------------ |
| Authorization | string | Your authorization token |

#### Request Body

| Name | Type   | Description               |
| ---- | ------ | ------------------------- |
| name | string | The name of the API Token |

{% tabs %}
{% tab title="200 " %}

```
{
    "status":"ok",
    "data":
    {
        "id":"1438281",
        "name":"Postman",
        "creation_date":1602515387862,
        "expiration_date":1665587387862,
        "value":"token value"
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Please note that you need an other api token to generate long-term api tokens using the API. You can generate the first one in the Navigator UI.
{% endhint %}

### List tokens using the API

<mark style="color:blue;">`GET`</mark> `https://api.mediarithmics.com/v1/users/:userId/api_tokens`

#### Path Parameters

| Name     | Type   | Description        |
| -------- | ------ | ------------------ |
| user\_id | string | The ID of the user |

#### Headers

| Name          | Type   | Description              |
| ------------- | ------ | ------------------------ |
| Authorization | string | Your authorization token |

{% tabs %}
{% tab title="200 " %}

```
{
    "status":"ok",
    "data":[
        {
            "id":"1483281",
            "name":"Postman",
            "creation_date":1602515387862,
            "expiration_date":1665587387862,
            "value":"api:gkqKkuXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXGcdKn7Vv9G"
        }],
    "count":1,
    "total":1,
    "first_result":0,
    "max_result":50,
    "max_results":50
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You won't be able to see the complete value of the tokens using this API.
{% endhint %}

## Signature authentication

The Signature authentication scheme is used in an environment where the calling part operates in a public network or published mobile or CTV app, and the communication with the mediarithmics platform could be hacked by a man in the middle or the app decompiled. It is the preferred choice for **app tracking.**

{% hint style="warning" %}
Signature authentication is limited to 2 actions only:

* Posting activities using the `POST /v1/datamarts/${datamart_id}/user_activities` endpoint
* Getting user segments by user account ID, email hash or user agent ID (not UserPoint ID) using `GET /v1/datamarts/:datamartId/user_points/compartmentId=:compartmentId,user_account_id=:userAccountId/user_segments`

  , or `GET /v1/datamarts/:datamartId/user_points/email_hash=:emailHash/user_segments` or `GET /v1/datamarts/:datamartId/user_points/user_agent_id=:userAgentId/user_segments` endpoints
  {% endhint %}

The authentication of the calling party (the app) is done through a signature (Message Authentication Code) using a hashing algorithm (`HMAC-SHA256`).

Signature authentication is linked to a mediarithmics user. As for the API token approach, we recommend you create a technical user on mediarithmics to bear this authentication scheme.

### Generate a secret key

<mark style="color:green;">`POST`</mark> `https://api.mediarithmics.com/v1/users/:userId/message_authentication_keys`

#### Request Body

| Name    | Type   | Description                                                                                                                                                                            |
| ------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| scheme  | string | The scheme associated with the secret key. Possible values : `HMAC_SHA256`                                                                                                             |
| key\_id | string | A unique string identifying your secret key. The API will return an error if the value is already used by another secret key. This value will be used later on when signing API calls. |

#### **Response Body**

| id             | String      | Record Id                                                                                                                                                                                                                                                              |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| user\_id       | String      | Id of the User linked to the secret key                                                                                                                                                                                                                                |
| creation\_ts   | String      | Date of creation                                                                                                                                                                                                                                                       |
| expiration\_ts | String      | Date of expiration                                                                                                                                                                                                                                                     |
| key\_id        | String      | A unique string identifying your secret key. Provided in the request by the user.                                                                                                                                                                                      |
| scheme         | String enum | The scheme associated with the secret key. Possible values: `HMAC_SHA256`                                                                                                                                                                                              |
| secret\_key    | String enum | The value of the secret key. It has a length of 16 bytes and is provided as a hexadecimal string **Warning:** Store this value as the value of the secret key is only visible in the response of the creation request. In all subsequent calls, it will be obfuscated. |

#### Usage

For each API call, a digest of the message should be calculated. Then, the 3 following headers should be contained in the API call:

* `X-Mics-Mac`: the digest of the input data (Message Authentication Code)
* `X-Mics-Key-Id`: the key\_id of the secret key (provided by you when creating a new secret key)
* `X-Mics-Ts`: the timestamp used in the digest (in ms)

#### Digest calculation

We recommend you find a library that generates digests for your platform. This library will need 2 input in order to work:

* **A key.** You should use the `secret_key` provided by the platform.
* **A message to sign.** you should generate the message from the info contained in the API call payload (see below).

The digest should be presented as a `Base64` string.

#### Message generation

The message to sign should be generated from the following values, in this order, separated by a `\n` (e.g. `${uri}\n${key_id}\n${timestamp}\n${request_body}`):

* The **uri** of the API method used
* The **key\_id** value (provided by you when creating the secret key)
* A **timestamp** (ms) generated at the time of the call
* The **request body** as a string

### Examples

#### Signing the message

These values:

* uri: /v1/datamarts/854/user\_activities
* key id: my\_key\_identifier
* timestamp: 1499103950000
* request body: {\\"hello\\":\\"world\\"}

Will translate to the following message:

```
/v1/datamarts/854/user_activities
my_key_identifier
1499103950000
{"hello":"world"}
```

Therefore, the calculation of the `HMAC-SHA256` of this message, with the secret key `846cee8e-5558-4ca0-b723-095aa043c6ee`, `base64` encoded, will be `rwhKdaWtw5Hx3zjcrZDv7eO4fyNbBkIfsh2PjI+BiRE=`.

### Mobile App

#### Posting activities for iOS using Swift

{% code fullWidth="false" %}

```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()
}


```

{% endcode %}

#### Posting activities for Android using Kotlin

```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()
    }
}
```

#### Posting activities in Node.js

```javascript
var rp = require('request-promise');
var crypto = require('crypto');

var datamart_id = 854;
var app_id = "154"; // string only
var domain = `https://api.mediarithmics.com`
var uri = `/v1/datamarts/${datamart_id}/user_activities`;
var secret_key = "846cee8e-5558-4ca0-b723-095aa043c6ee";				 
var key_id = "my_key_identifier";
var ts = 1499103950000; // in milliseconds
var test_device = {
    "os": "and",
    "aaid": "b9f47db8-c0e9-40f4-8aed-fb063d7e54f7"
};
var body = {
    "$ts": ts,
    "$type": "APP_VISIT",
    "$session_status": "IN_SESSION",
    "$user_agent_id": `mob:${test_device.os}:raw:${test_device.aaid}`,
    "$app_id": app_id,
    "$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": null,
        "$agent_type": "MOBILE_APP"
    },
    "$events": [{
        "$ts": ts,
        "$event_name": "my_super_event",
        "$properties": {
            "custom_property1": "value1",
            "custom_property2": "value2",
            "custom_property3": "value3",
            "custom_property4": "value4",
            "custom_property5": "value5"
        }
    }]
};
var headers = {
    "X-Mics-Key-Id": key_id,
    "X-Mics-Ts": ts
};

var message = uri + "\n" + key_id + "\n" + JSON.stringify(ts) + "\n" + JSON.stringify(body);
var signature = signKey(secret_key, message);

headers["X-Mics-Mac"] = signature;

console.log(`uri: ${uri}\nsecret_key: ${secret_key}\nkey_id: ${key_id}\nts: ${JSON.stringify(ts)}\nheaders: ${JSON.stringify(headers)}\nbody: ${JSON.stringify(body)}`)
console.log(`message: ${message}`);
console.log(`signature: ${signature}`);

postEvent(domain + uri,'POST',headers,body);

function signKey(clientKey, message) {
    var hash = crypto.createHmac('sha256', clientKey).update(message).digest('base64');
    return hash;
};

async function postEvent(uri, method, headers, body) {    
    var request = await rp({
        uri: uri,
        method: method,
        headers: headers,
        body: body,
        json: true
    });
    console.log(request)
}
```

#### Getting user segments in Node.js

```javascript
var rp = require('request-promise');
var crypto = require('crypto');

var datamart_id = 854;
var user_agent_id = 'vec:xxx';
var domain = `https://api.mediarithmics.com`;
var uri = `/v1/datamarts/${datamart_id}/user_points/user_agent_id=${user_agent_id}/user_segments`;
var secret_key = "846cee8e-5558-4ca0-b723-095aa043c6ee";				 
var key_id = "my_key_identifier";
var ts = 1499103950000; // in milliseconds
var headers = {
    "X-Mics-Key-Id": key_id,
    "X-Mics-Ts": ts
};

var message = uri + "\n" + key_id + "\n" + JSON.stringify(ts);
var signature = signKey(secret_key, message);

headers["X-Mics-Mac"] = signature;

console.log(`uri: ${uri}\nsecret_key: ${secret_key}\nkey_id: ${key_id}\nts: ${JSON.stringify(ts)}\nheaders: ${JSON.stringify(headers)}`)
console.log(`message: ${message}`);
console.log(`signature: ${signature}`);

getEvent(domain + uri,'GET',headers);

function signKey(clientKey, message) {
    var hash = crypto.createHmac('sha256', clientKey).update(message).digest('base64');
    return hash;
};

async function getEvent(uri, method, headers) {    
    var request = await rp({
        uri: uri,
        method: method,
        headers: headers,
        json: true
    });
    console.log(request)
}
```

### CTV app

#### Posting activities in JavaScript for Tizen and webOS

```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);
```

#### Posting activities in Kotlin for Android TV/Google TV

```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()
```

#### Posting activities in BrightScript for Roku TV

```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", "")
```
