# Trillboards CTV Measurement SDK — Quickstart

Audience-measurement SDK for Connected TV. Two runtimes share one wire format:

- **`@trillboards/ctv-measurement`** — npm package for CTV-web ad units, smart-TV
  apps (Samsung Tizen, LG webOS, Vizio SmartCast, Fire TV / Google TV / Android TV
  WebView), and any HTML5 surface running inside an ATSC 3.0 BA or HbbTV runtime.
- **`com.github.trillboards.packages:ctv-measurement`** — Android AAR for native
  Android TV / Fire TV / tablet apps. JitPack-fetched. Built on
  `agent-core-lite`, the same proximity stack the Trillboards in-house fleet
  runs in production today.

Both SDKs POST to the same heartbeat endpoint:
`POST /v1/partner/device/{deviceId}/heartbeat`. Get a `tb_ctv_*` API key at
`https://trillboards.com/partners/get-started` (self-serve, no email round-trip).

## Step 1 — Get an API key

```bash
curl -X POST https://api.trillboards.com/v1/partner/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Your Company",
    "slug": "your-company",
    "contact_email": "dev@example.com",
    "partner_type": "ctv_publisher",
    "api_agreement_accepted": true
  }'
```

The response contains a one-time `api_key` of the form `tb_ctv_xxxxx`. Store it in
your secret manager — Trillboards does not keep a plaintext copy.

You can also use the web form at <https://trillboards.com/partners/get-started>.

## Step 2 — Install the SDK

### npm (CTV web, React Native, smart-TV web apps, ATSC 3.0 / HbbTV runtimes)

```bash
npm install @trillboards/ctv-measurement
```

```ts
import { CtvMeasurement } from '@trillboards/ctv-measurement';

const sdk = new CtvMeasurement({
  apiKey: 'tb_ctv_YOUR_KEY',
  deviceId: 'your-device-id',
});

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

// Captures fingerprint + viewability + connection + (if the runtime is an
// ATSC 3.0 BA or HbbTV receiver) broadcaster signaling, then POSTs the
// heartbeat. Returns the upload result.
await sdk.measure();
```

### Gradle / JitPack (Android native — Android TV, Fire TV, tablet apps)

In your **root `settings.gradle.kts`** (or `settings.gradle`):

```kotlin
dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral()
    maven { url = uri("https://jitpack.io") }
  }
}
```

In your **app-module `build.gradle.kts`**:

```kotlin
dependencies {
  implementation("com.github.trillboards.packages:ctv-measurement:0.2.1")
}
```

In your **`MainActivity.kt`** (or `Application.onCreate`):

```kotlin
import com.trillboards.measurement.TrillboardsMeasurement

TrillboardsMeasurement.initialize(
  context = this,
  apiKey = "tb_ctv_YOUR_KEY",
  deviceId = "your-device-id",
)
TrillboardsMeasurement.setConsentStatus(true)
TrillboardsMeasurement.startScheduledScans()
```

`startScheduledScans()` runs the BLE / WiFi / mDNS / SSDP / HTTP-probe scanners on
a 30-second cadence and POSTs each snapshot as a heartbeat. The scanners use
`agent-core-lite`, the same code that powers the Trillboards in-house tablet
fleet (~658K signal observations per day across 10 active screens).

### Required Android permissions

Add to `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

<!-- BLE scanning (Android 12+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                 android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- BLE scanning (Android 11 and below) -->
<uses-permission android:name="android.permission.BLUETOOTH"
                 android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                 android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
                 android:maxSdkVersion="30" />
```

## Step 3 — Consent gate

**Both SDKs are inert until `setConsentStatus(true)` is called.** No network
traffic, no scanning, no fingerprinting. This is the GDPR / CCPA gate; surface
it through your existing consent flow.

To revoke consent:

```ts
sdk.setConsentStatus(false); // npm
```

```kotlin
TrillboardsMeasurement.setConsentStatus(false) // Android
```

When consent is revoked, in-flight requests complete but no new measurements
are emitted until consent is granted again.

## Step 4 — Verify your integration

After one heartbeat lands, run this against ClickHouse to confirm rows are
arriving for your device:

```sql
SELECT source, count() AS rows
FROM trillboards.signal_observations
WHERE screen_mongo_id = 'your-device-id'
  AND observed_at > now() - INTERVAL 5 MINUTE
GROUP BY source
ORDER BY rows DESC;
```

You should see rows for `fingerprint` (every runtime) plus any of
`ble` / `wifi` / `mdns` / `ssdp` / `http_probe` your platform supports
(Android native covers all five; CTV web covers `fingerprint` + connection +
viewability + `broadcast` when running inside an ATSC 3.0 BA or HbbTV runtime).

## Endpoints

The SDKs call these endpoints automatically. All four accept the same
`tb_ctv_*` key via the `Authorization: Bearer <key>` header.

| Method | Path                                         | Purpose                                              |
|--------|----------------------------------------------|------------------------------------------------------|
| POST   | `/v1/partner/register`                       | Self-serve `tb_ctv_*` key minting (one-time)         |
| POST   | `/v1/partner/device/{deviceId}/heartbeat`    | Audience-measurement heartbeat (every 30 s)          |
| POST   | `/v1/partner/api-key/rotate`                 | Rotate a `tb_ctv_*` key without downtime             |
| GET    | `/v1/partner/info`                           | Read the partner record (slug, scoped regions, etc.) |

Full OpenAPI: <https://api.trillboards.com/docs/openapi/partner-api.yaml>.

## Platform target matrix

The npm SDK feature-probes the host runtime at boot and selects the right
adapter. Every adapter ships fingerprint + viewability + connection; some add
broadcaster signaling.

| Runtime                                  | Universal core | Broadcast metadata           | Path        |
|------------------------------------------|----------------|------------------------------|-------------|
| ATSC 3.0 Broadcaster Application         | yes            | yes — A/344 `localhost:` WS  | npm SDK     |
| HbbTV 2.0+ receiver                      | yes            | yes — `oipfObjectFactory`    | npm SDK     |
| Samsung Tizen TV web app                 | yes            | no (privileged only)         | npm SDK     |
| LG webOS TV web app                      | yes            | no (privileged only)         | npm SDK     |
| Vizio SmartCast web app                  | yes            | no                           | npm SDK     |
| Fire TV / Google TV / Android TV WebView | yes            | no (bridge to native)        | npm SDK     |
| Android TV / Fire TV native app          | yes            | n/a                          | Android AAR |
| Android tablet / phone (Naki proxy)      | yes            | n/a                          | Android AAR |
| Roku BrightScript / SceneGraph           | n/a            | n/a                          | unsupported |
| Apple tvOS (TVMLKit / JSC)               | partial        | n/a                          | unsupported |

For runtimes marked **unsupported**, contact us at
<https://trillboards.com/support/contact> — these need a separate native
adapter and are out of scope for v1.

## Privacy posture

The SDK is privacy-first by design:

- **No PII collection.** BLE scanning counts nearby devices; it does NOT
  resolve them to a person. Hashed MACs are dropped server-side after
  k-anon-5 aggregation.
- **No camera, no microphone, no audio.** The public `ctv-measurement` AAR
  ships zero CV / audio code. The Trillboards in-house fleet SDK
  (`agent-core`, distributed via GitHub Packages) is a separate package with
  a separate consent contract.
- **K-anon-5 enforced server-side.** Aggregate exports require at least 5
  distinct devices per geohash × hour × venue-type bucket.
- **Consent revocation halts new measurements** in flight; no offline queue
  drains after consent is revoked.

For the canonical privacy & data-handling write-up, see
<https://api.trillboards.com/docs/integrations/partner-native-bridge.md>.

## Next steps

- Manage, rotate, and revoke your `tb_ctv_*` key in the earner portal at
  <https://trillboards.com/earner> under **Developer → API Keys**.
- Read the partner integration deep-dive at
  <https://api.trillboards.com/docs/integrations/ctv-measurement-partner-integration.md>.
- Browse the OpenAPI spec at
  <https://api.trillboards.com/docs/openapi/partner-api.yaml>.
- Inspect the full machine-readable agent guide at
  <https://api.trillboards.com/llms.txt>.
