Partner Native Bridge Protocol

The Trillboards Lite SDK includes a Universal Native Bridge that enables seamless communication between the SDK (running in a WebView/iframe) and your native host application. This allows you to receive ad events and send commands from Android, iOS, React Native, Flutter, CTV, Electron, and other platforms.

Overview

When embedding the Trillboards Lite SDK in a native application, you need a way to:

  1. Receive events when ads start, complete, error, or have no fill
  2. Send commands to show/hide ads, skip, or refresh

The Native Bridge handles this by:

Quick Start (Zero Config)

For most platforms, the SDK auto-detects the native bridge. Just expose the standard interface in your native code:

Android WebView


// In your WebView setup
webView.addJavascriptInterface(object {
    @JavascriptInterface
    fun onEvent(payload: String) {
        val event = JSONObject(payload)
        when (event.getString("event")) {
            "ad_started" -> handleAdStarted(event.getJSONObject("data"))
            "ad_completed" -> handleAdCompleted(event.getJSONObject("data"))
            "ad_error" -> handleAdError(event.getJSONObject("data"))
            "no_ads" -> handleNoAds(event.getJSONObject("data"))
        }
    }
}, "TrillboardsBridge")

iOS WKWebView


// In your WKWebView setup
let contentController = webView.configuration.userContentController
contentController.add(self, name: "trillboards")

// Implement WKScriptMessageHandler
extension YourViewController: WKScriptMessageHandler {
    func userContentController(_ controller: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        guard message.name == "trillboards",
              let body = message.body as? [String: Any],
              let event = body["event"] as? String else { return }

        switch event {
        case "ad_started": handleAdStarted(body["data"] as? [String: Any])
        case "ad_completed": handleAdCompleted(body["data"] as? [String: Any])
        case "ad_error": handleAdError(body["data"] as? [String: Any])
        case "no_ads": handleNoAds(body["data"] as? [String: Any])
        default: break
        }
    }
}

React Native WebView


import { WebView } from 'react-native-webview';

function AdPlayer() {
  const handleMessage = (event: WebViewMessageEvent) => {
    const data = JSON.parse(event.nativeEvent.data);
    if (data.type !== 'trillboards') return;

    switch (data.event) {
      case 'ad_started':
        console.log('Ad started:', data.data);
        break;
      case 'ad_completed':
        console.log('Ad completed:', data.data);
        break;
      case 'ad_error':
        console.log('Ad error:', data.data);
        break;
      case 'no_ads':
        console.log('No ads available:', data.data);
        break;
    }
  };

  return (
    <WebView
      source={{ uri: 'https://screen.trillboards.com/partner-sdk/embed.html?device=YOUR_DEVICE_ID' }}
      onMessage={handleMessage}
      javaScriptEnabled
      mediaPlaybackRequiresUserAction={false}
    />
  );
}

Flutter WebView


import 'package:webview_flutter/webview_flutter.dart';
import 'dart:convert';

class AdPlayerWidget extends StatefulWidget {
  @override
  _AdPlayerWidgetState createState() => _AdPlayerWidgetState();
}

class _AdPlayerWidgetState extends State<AdPlayerWidget> {
  late WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return WebViewWidget(
      controller: WebViewController()
        ..setJavaScriptMode(JavaScriptMode.unrestricted)
        ..addJavaScriptChannel(
          'Flutter',
          onMessageReceived: (message) {
            final data = jsonDecode(message.message);
            if (data['type'] != 'trillboards') return;

            switch (data['event']) {
              case 'ad_started':
                print('Ad started: ${data['data']}');
                break;
              case 'ad_completed':
                print('Ad completed: ${data['data']}');
                break;
              case 'ad_error':
                print('Ad error: ${data['data']}');
                break;
              case 'no_ads':
                print('No ads: ${data['data']}');
                break;
            }
          },
        )
        ..loadRequest(Uri.parse(
          'https://screen.trillboards.com/partner-sdk/embed.html?device=YOUR_DEVICE_ID'
        )),
    );
  }
}

Auto-Detection Matrix

The SDK automatically detects these interfaces (in order):

PlatformDetectionInterface
Android WebViewwindow.TrillboardsBridge.onEventTrillboardsBridge.onEvent(jsonString)
Android (alt)window.Android.onTrillboardsEventAndroid.onTrillboardsEvent(jsonString)
iOS WKWebViewwindow.webkit.messageHandlers.trillboardstrillboards.postMessage(object)
React Nativewindow.ReactNativeWebView.postMessageReactNativeWebView.postMessage(jsonString)
Flutterwindow.Flutter.postMessageFlutter.postMessage(jsonString)
CTV (Tizen/webOS/Fire TV)window.__TRILL_CTV_BRIDGE__.postEvent__TRILL_CTV_BRIDGE__.postEvent(jsonString)
Electronwindow.electronAPI.trillboardsEventelectronAPI.trillboardsEvent(object)
Tauriwindow.__TAURI__.eventEmits trillboards-event
Iframe embedwindow.parent !== windowparent.postMessage(object, '*')

Custom Bridge Registration

For non-standard integrations, register a custom bridge:


// In your WebView's JavaScript context
TrillboardsLite.registerBridge({
    // Required: how to send events to host
    send: function(event, data) {
        // Your custom transport
        MyCustomBridge.sendEvent(JSON.stringify({ event, data }));
    },

    // Optional: receive commands from host
    receive: function(callback) {
        // Set up listener for host commands
        MyCustomBridge.onCommand = callback;
    },

    // Optional: filter which events to send (reduces noise)
    events: ['ad_started', 'ad_completed', 'ad_error', 'no_ads'],

    // Optional: bridge name for debugging
    name: 'MyCustomBridge'
});

Event Protocol

All events follow this standardized payload structure:


interface TrillboardsEvent {
    type: 'trillboards';           // Message type identifier
    version: '1.0';                // Protocol version
    event: string;                 // Event name (see table below)
    data: object;                  // Event-specific data
    timestamp: number;             // Unix timestamp (ms)
    deviceId: string;              // Device identifier
    screenId?: string;             // Screen identifier (if linked)
    sessionId: string;             // Session identifier
}

Events Reference

EventDescriptionData
readySDK initialized{ deviceId, screenId, adCount, waterfallMode }
ad_startedAd began playing{ type: 'programmatic'|'image'|'video', adId?, url? }
ad_completedAd finished naturally{ type, reason: 'complete', completed: true }
ad_skippedUser skipped ad{ type }
ad_endedAd playback ended{ type?, reason? }
ad_errorAd failed{ type, error: { message, code? } }
no_adsNo fill available{ reason: 'no_fill'|'offline'|'programmatic_unavailable' }
overlay_clickedUser tapped CTA{}
ads_refreshedAd list updated{ count }
onlineNetwork restored{}
offlineNetwork lost{}
state_changedState responseFull state object (see getState())

Sending Commands to SDK

Your native app can send commands to control the SDK.

From Android


// Show an ad
webView.evaluateJavascript("TrillboardsLite.show()", null)

// Hide the player
webView.evaluateJavascript("TrillboardsLite.hide()", null)

// Skip current ad
webView.evaluateJavascript("TrillboardsLite.skipAd()", null)

// Refresh ad list
webView.evaluateJavascript("TrillboardsLite.refresh()", null)

From iOS


// Show an ad
webView.evaluateJavaScript("TrillboardsLite.show()")

// Hide the player
webView.evaluateJavaScript("TrillboardsLite.hide()")

From React Native


const webViewRef = useRef<WebView>(null);

// Show an ad
webViewRef.current?.injectJavaScript('TrillboardsLite.show(); true;');

// Hide the player
webViewRef.current?.injectJavaScript('TrillboardsLite.hide(); true;');

Via postMessage (Iframe)


// From parent window to iframe
const iframe = document.getElementById('trillboards-iframe');

// Show an ad
iframe.contentWindow.postMessage({
    type: 'trillboards-command',
    action: 'show'
}, '*');

// Hide the player
iframe.contentWindow.postMessage({
    type: 'trillboards-command',
    action: 'hide'
}, '*');

// Get current state (response via state_changed event)
iframe.contentWindow.postMessage({
    type: 'trillboards-command',
    action: 'getState'
}, '*');

// Update configuration
iframe.contentWindow.postMessage({
    type: 'trillboards-command',
    action: 'configure',
    params: { waterfall: 'programmatic_only', volume: 0 }
}, '*');

Command Reference

CommandDescriptionParameters
showRequest and play an ad{ immediate?: boolean }
hideStop and hide player{ silent?: boolean }
skipSkip current ad{}
refreshRefresh ad list from API{}
getStateRequest current state{} (responds with state_changed)
configureUpdate settings{ waterfall?, volume? }

Platform-Specific Guides

CTV (Smart TVs)

For Tizen (Samsung), webOS (LG), Fire TV, and Android TV:


// On your CTV app, expose this interface before loading the WebView
window.__TRILL_CTV_BRIDGE__ = {
    postEvent: function(jsonString) {
        // Parse and route to your CTV app's event system
        const event = JSON.parse(jsonString);
        TizenBridge.sendEvent(event.event, event.data);
    }
};

Electron


// In preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
    trillboardsEvent: (data) => {
        ipcRenderer.send('trillboards-event', data);
    }
});

// In main.js
ipcMain.on('trillboards-event', (event, data) => {
    console.log('Trillboards event:', data.event, data.data);
});

Tauri


// In your Tauri app
use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let window = app.get_window("main").unwrap();
            window.listen("trillboards-event", |event| {
                println!("Trillboards event: {:?}", event.payload());
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Capacitor/Cordova


// Using @capawesome/capacitor-app-webview
import { AppWebview } from '@capawesome/capacitor-app-webview';

// Listen for messages
AppWebview.addListener('message', (event) => {
    const data = JSON.parse(event.message);
    if (data.type === 'trillboards') {
        console.log('Trillboards event:', data.event);
    }
});

Event Filtering

To reduce noise, filter events at registration:


TrillboardsLite.registerBridge({
    send: (event, data) => { /* ... */ },
    // Only receive these critical events
    events: ['ad_started', 'ad_completed', 'ad_error', 'no_ads']
});

Best Practices

1. Handle Offline Gracefully

The SDK caches ads for offline playback. Listen for online/offline events:


"offline" -> {
    // Show offline indicator or pause ad requests
}
"online" -> {
    // Resume normal operation
}

2. Track Session State

Use the sessionId in events to correlate ads within a user session:


val sessionMetrics = mutableMapOf<String, MutableList<String>>()

fun handleEvent(event: JSONObject) {
    val sessionId = event.getString("sessionId")
    val eventName = event.getString("event")
    sessionMetrics.getOrPut(sessionId) { mutableListOf() }.add(eventName)
}

3. Implement Retry Logic

When no_ads occurs, the SDK automatically retries in programmatic_only mode. For manual control:


"no_ads" -> {
    // SDK handles retry automatically, but you can track
    Log.d("Trillboards", "No fill - SDK will retry")
}

4. Clean Up on Destroy


override fun onDestroy() {
    webView.evaluateJavascript("TrillboardsLite.destroy()", null)
    super.onDestroy()
}

Troubleshooting

Events Not Received

  1. Check interface name: Ensure your bridge uses the exact name the SDK expects
  2. Check timing: Register your interface before the WebView loads the SDK
  3. Check console: Look for [Trillboards] Bridge auto-detected: log
  4. Use custom bridge: If auto-detection fails, register explicitly

Commands Not Working

  1. Check WebView is loaded: Wait for page load before sending commands
  2. Check JavaScript enabled: Ensure javaScriptEnabled = true
  3. Check console errors: Look for JavaScript errors in WebView

Debugging

Enable verbose logging:


// In WebView console
console.log('[Trillboards] Bridge detected:', window.TrillboardsBridge ? 'yes' : 'no');
console.log('[Trillboards] State:', TrillboardsLite.getState());

FAQ

Is /programmatic-event API for native communication?

No. The /device/{deviceId}/programmatic-event endpoint is a server-side analytics endpoint. It records events for Trillboards' internal analytics and triggers webhooks. It does NOT push events to your native app.

For native app event delivery, use the JavaScript Native Bridge described in this document.

Can I use both webhooks and native bridge?

Yes. They serve different purposes:

What if my platform isn't listed?

Use the custom bridge registration:


TrillboardsLite.registerBridge({
    send: (event, data) => {
        // Your custom transport mechanism
    }
});

How do I test the bridge?

  1. Load the SDK in your WebView
  2. Open developer console
  3. Trigger an ad: TrillboardsLite.show()
  4. Check your native handler receives events