Trillboards
Back to Developers
Developer Docs

CTV Measurement SDK

Privacy-first audience measurement for Connected TV. BLE proximity sensing, census enrichment, QR attribution, and MRC-compliant viewability tracking for Android TV and Fire TV apps.

npm
npm install @trillboards/ctv-measurement
build.gradle.kts (Android)
dependencies {
    implementation("com.trillboards:measurement-sdk:0.1.0")
}

Capabilities

BLE Proximity

Bluetooth Low Energy device scanning counts nearby mobile devices for audience measurement without any PII collection.

Census Enrichment

IP-based demographic enrichment using US Census ACS data at geohash-6 resolution. IAB income, age, and education segments.

Device Fingerprinting

Canvas hash, WebGL renderer, audio sample rate, and hardware signals for cross-session device identity without cookies.

Viewability Tracking

MRC-compliant viewability with completion ratio, duration thresholds, and IMA impression confirmation for CTV environments.

QR Attribution

Physical-to-digital attribution via QR code scanning. IP co-location matching links the scanning mobile device to the CTV screen.

Connection Intelligence

Network classification (residential, mobile, commercial, VPN) with effective type, downlink speed, and RTT measurements.

Session Management

Persistent session IDs from SDK create() to destroy(). Per-household frequency capping and daily reach deduplication.

GDPR/CCPA Consent

Consent-first architecture. All measurement is inert until the publisher calls setConsentStatus(true). No data leaves the device without consent.

Quick Start

From install to first enriched impression in 5 steps.

Step 1: Install the SDK

Terminal
npm install @trillboards/ctv-measurement

Step 2: Initialize with consent

app.js
import { CtvMeasurement } from '@trillboards/ctv-measurement';

const sdk = CtvMeasurement.create({
  apiKey: 'tb_ctv_YOUR_API_KEY',
  channel: 'ctv',
});

// Consent-first: measurement is inert until this is called
sdk.setConsentStatus(true);

Step 3: Enrich with census data

enrich.js
// Server-side IP lookup returns IAB demographic segments
const enrichment = await sdk.enrich();

console.log(enrichment.iabIncomeSegmentName);
// => "Upper-Middle Income ($100-150k)"
console.log(enrichment.iabAgeSegmentName);
// => "Age 35-44"
console.log(enrichment.connectionType);
// => "residential"

Step 4: Track an impression

impression.js
const result = await sdk.trackImpression({
  adId: 'ad_summer_sale_15s',
  placementId: 'living_room_main',
  viewable: true,
  viewabilityReason: 'ctv_full_completion',
  completionRatio: 1.0,
  durationMs: 15000,
  bleDeviceCount: 3,
  bleProximityConfidence: 0.85,
});

console.log(result.givtPassed);     // true (not a duplicate)
console.log(result.frequency);      // 2 (household saw this ad twice today)
console.log(result.householdReach); // 1847 (distinct households for this ad)

Step 5: QR attribution (optional)

qr.js
// When a viewer scans the QR code on the CTV screen,
// the mobile browser hits your landing page with the scanId
const scan = await sdk.recordQrScan({
  scanId: 'c4a760a8-dbce-443e-a96a-4f2b1e5f0a3b',
  screenId: 'screen_lobby_01',
  adId: 'ad_summer_sale_15s',
  placementId: 'living_room_main',
  destinationUrl: 'https://brand.example.com/offer',
});

// The mobile beacon fires automatically from the landing page
// to perform IP co-location matching
console.log(scan.id); // PG row id for the scan event

Android Integration

The Kotlin AAR wraps the same measurement pipeline for native Android TV and Fire TV apps. Add the dependency and initialize in your Application class.

build.gradle.kts
dependencies {
    implementation("com.trillboards:measurement-sdk:0.1.0")
}
MeasurementActivity.kt
import com.trillboards.measurement.CtvMeasurement

class MeasurementActivity : AppCompatActivity() {
    private lateinit var sdk: CtvMeasurement

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sdk = CtvMeasurement.create(
            apiKey = "tb_ctv_YOUR_API_KEY",
            channel = "ctv"
        )

        // Consent-first: measurement is inert until this is called
        sdk.setConsentStatus(true)

        // Enrich with census demographics
        lifecycleScope.launch {
            val enrichment = sdk.enrich()
            Log.d("CTV", "Income: ${enrichment.iabIncomeSegmentName}")
            Log.d("CTV", "Connection: ${enrichment.connectionType}")
        }

        // Track an impression
        lifecycleScope.launch {
            val result = sdk.trackImpression(
                adId = "ad_summer_sale_15s",
                placementId = "lobby_screen_main",
                viewable = true,
                completionRatio = 1.0,
                durationMs = 15000,
                bleDeviceCount = 3
            )
            Log.d("CTV", "GIVT passed: ${result.givtPassed}")
            Log.d("CTV", "Frequency: ${result.frequency}")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        sdk.destroy()
    }
}

API Endpoints

The SDK calls these endpoints automatically. For server-to-server integrations, you can call them directly with your API key.

MethodEndpointDescription
POST/v2/sdk/enrichCensus demographic enrichment from request IP
POST/v2/sdk/qr/scanRecord a QR scan event
POST/v2/sdk/qr/beaconProcess beacon signals from mobile QR scan
POST/v2/sdk/impressionIngest enriched CTV impression with GIVT dedup

Authentication

All endpoints require an API key with the tb_ctv_ prefix. Include it as a header on every request:

Authentication
# X-API-Key header
curl -X POST https://api.trillboards.com/v2/sdk/enrich \
  -H "X-API-Key: tb_ctv_YOUR_KEY" \
  -H "Content-Type: application/json"

API keys are provisioned with scoped permissions: sdk:enrich, sdk:qr, and sdk:impression. Contact developers@trillboards.com to get your CTV publisher API key.

Rate Limits

EndpointLimit
/v2/sdk/enrich300/min per API key
/v2/sdk/qr/scan100/min per API key
/v2/sdk/qr/beacon200/min per API key
/v2/sdk/impression1,000/min per API key

Rate limit headers are included in every response: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. When rate limited, the response includes a Retry-After header with the number of seconds to wait.

Ready to measure your CTV audience?

Explore the full API reference with interactive request builders and response examples.

Need help integrating? Contact developers@trillboards.com