Skip to main content

Kotlin SDK Integration Guide

Integrate WatchTower monitoring into your Android POS terminal app in under 15 minutes. The SDK handles gRPC connection, heartbeats, GPS tracking, offline queuing, and reconnection automatically.

Requirements

  • Android 5.0+ (API 21+)
  • No Google Play Services required (works on AOSP terminals)
  • An agent API key from the WatchTower dashboard

Step 1: Add the Dependency

Add the WatchTower SDK to your app’s build.gradle.kts:
dependencies {
    implementation("io.watchtower:agent-sdk:1.0.0")
}
Or if you’re building from source (monorepo):
dependencies {
    implementation(project(":sdks:kotlin"))
}

Step 2: Add Permissions

The SDK needs these permissions in your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Note: If your app already has these permissions (most POS apps do), no changes needed.

Step 3: Initialize the Agent

Create the agent in your Application class or main Activity:
import io.watchtower.sdk.WatchTowerAgent
import io.watchtower.sdk.WatchTowerConfig
import io.watchtower.sdk.AgentCallback

class MyTerminalApp : Application() {

    lateinit var agent: WatchTowerAgent

    override fun onCreate() {
        super.onCreate()

        val config = WatchTowerConfig(
            serverHost = "grpc.watchtower.io",  // Your WatchTower server
            serverPort = 9090,
            apiKey = "wt_agent_your_key_here",   // From Settings → API Keys
            terminalId = "SN-001",               // Your terminal serial number
            useTls = true,                       // TLS in production
            heartbeatIntervalSeconds = 30,       // Send heartbeat every 30s
        )

        agent = WatchTowerAgent.create(this, config, MyAgentCallback())
        agent.start()
    }
}

Step 4: Handle Platform Commands

Implement AgentCallback to react to commands from the WatchTower platform:
import io.watchtower.sdk.AgentCallback

class MyAgentCallback : AgentCallback {

    override fun onConnected() {
        Log.i("WatchTower", "Connected to platform")
    }

    override fun onDisconnected(reason: String) {
        Log.w("WatchTower", "Disconnected: $reason")
    }

    override fun onReconnecting(attempt: Int, delayMs: Long) {
        Log.i("WatchTower", "Reconnecting in ${delayMs}ms (attempt $attempt)")
    }

    override fun onBlockCommand(reason: String, blockType: String) {
        Log.w("WatchTower", "BLOCKED: $reason (type: $blockType)")

        // Disable payment processing in your app
        PaymentProcessor.disable()

        // Show message to operator
        showOperatorAlert("Terminal blocked: $reason")
    }

    override fun onUnblockCommand(reason: String) {
        Log.i("WatchTower", "UNBLOCKED: $reason")

        // Re-enable payment processing
        PaymentProcessor.enable()
    }

    override fun onConfigUpdate(heartbeatIntervalSecs: Int, gpsIntervalSecs: Int) {
        Log.i("WatchTower", "Config updated: heartbeat=${heartbeatIntervalSecs}s")
        // SDK automatically adjusts intervals — no action needed
    }

    override fun onError(error: Throwable) {
        Log.e("WatchTower", "Agent error", error)
    }

    override fun onTransactionQueued(queueSize: Int) {
        Log.d("WatchTower", "Transaction queued (offline). Queue size: $queueSize")
    }

    override fun onQueueFlushed(count: Int) {
        Log.i("WatchTower", "Flushed $count queued transactions after reconnect")
    }
}

Step 5: Report Transactions

After each payment transaction completes, report it to WatchTower:
import io.watchtower.sdk.model.Transaction

// In your payment processing code:
fun onPaymentComplete(result: PaymentResult) {
    val transaction = Transaction(
        type = "purchase",                    // "purchase", "withdrawal", "transfer", "payment", "refund"
        amount = result.amountInKobo,         // Amount in minor units (kobo, cents)
        currency = "NGN",                     // ISO 4217 currency code
        status = if (result.approved) "approved" else "declined",
        reference = result.referenceNumber,   // Processor reference
        responseCode = result.responseCode,   // e.g., "00" for approval
    )

    // Get the agent instance
    (application as MyTerminalApp).agent.reportTransaction(transaction)
}
Offline behavior: If the terminal is disconnected, reportTransaction() automatically queues the transaction in local SQLite. When the connection is restored, queued transactions are flushed automatically (oldest first).

Step 6: Manual Controls (Optional)

val agent = (application as MyTerminalApp).agent

// Force an immediate heartbeat
agent.sendHeartbeatNow()

// Manually update GPS location (if your app has its own GPS)
agent.updateLocation(lat = 6.5244, lng = 3.3792)

// Check connection state
if (agent.isConnected) {
    // Connected to WatchTower
}

// Stop the agent (e.g., on app shutdown)
agent.stop()

Configuration Reference

ParameterTypeDefaultDescription
serverHostString(required)WatchTower gRPC server hostname
serverPortInt9090gRPC server port
apiKeyString(required)Agent API key (wt_agent_*)
terminalIdString(required)Terminal serial number
useTlsBooleantrueUse TLS for gRPC connection
heartbeatIntervalSecondsInt30Heartbeat frequency in seconds
gpsEnabledBooleantrueEnable GPS location tracking
offlineQueueEnabledBooleantrueQueue transactions when offline
maxOfflineQueueSizeInt10000Max queued events before oldest are dropped
reconnectEnabledBooleantrueAuto-reconnect on disconnect
reconnectMinDelayMsLong1000Minimum reconnect backoff (ms)
reconnectMaxDelayMsLong60000Maximum reconnect backoff (ms)

How Heartbeats Work

The SDK automatically sends heartbeats at the configured interval. Each heartbeat includes:
FieldSourceNotes
GPS coordinatesLocationManager or FusedLocationProviderFalls back gracefully if unavailable
Battery percentageBatteryManagerReads current battery level
Signal strengthTelephonyManagerCellular signal strength
UptimeSystem clockSeconds since agent started
GPS provider priority:
  1. Google FusedLocationProviderClient (if Play Services available — better accuracy)
  2. Android LocationManager with GPS_PROVIDER (works on all AOSP devices)
  3. Android LocationManager with NETWORK_PROVIDER (fallback)
  4. No location (heartbeats sent without GPS if all providers fail)

Offline Queue

When the terminal loses connectivity:
  • Transactions are queued in local SQLite (watchtower_queue.db)
  • Heartbeats are NOT queued (stale heartbeats are useless)
  • Queue is FIFO — oldest transactions sent first on reconnect
  • Max queue size is configurable (default 10,000)
  • When the queue is full, oldest events are dropped
On reconnect:
  1. Queue is flushed before normal operations resume
  2. onQueueFlushed(count) callback fires with the number of flushed events
  3. Regular heartbeats and transaction reporting continue

Reconnection Behavior

When the gRPC connection drops:
  1. onDisconnected(reason) callback fires
  2. SDK waits with exponential backoff: 1s → 2s → 4s → 8s → … → 60s (capped)
  3. Random jitter (0-500ms) added to prevent all terminals reconnecting simultaneously
  4. onReconnecting(attempt, delayMs) fires before each attempt
  5. On successful reconnect: onConnected() fires, offline queue is flushed
  6. Backoff resets to 1s on successful connection

Threading

  • All callbacks are dispatched on the Android main thread — safe to update UI directly
  • gRPC operations run on background IO threads
  • SQLite operations run on background threads
  • No threading concerns for your app code

Testing with the WatchTower Test Agent

Before integrating the SDK, you can test your WatchTower setup using the CLI test agent:
watchtower test-agent \
  --server localhost:9090 \
  --api-key wt_agent_your_key_here \
  --terminal-id SN-001 \
  --lat 6.5244 --lng 3.3792 \
  --heartbeat-interval 5s \
  --tx-interval 10s
This simulates a terminal sending heartbeats and transactions, letting you verify the dashboard, rules, and alerts work before deploying the SDK to real devices.

Troubleshooting

Agent won’t connect:
  • Verify the API key is correct and has agent scope
  • Verify the terminal serial number matches what’s registered in WatchTower
  • Check network connectivity and firewall rules (gRPC uses port 9090)
  • For local development, set useTls = false
No GPS data in heartbeats:
  • Ensure ACCESS_FINE_LOCATION permission is granted at runtime (Android 6.0+)
  • Check that GPS is enabled in device settings
  • The SDK falls back gracefully — heartbeats still send without GPS
Transactions not appearing in dashboard:
  • Check onError callback for errors
  • If offline, transactions are queued — they’ll appear after reconnect
  • Verify the terminal is registered and active in WatchTower
Block command not received:
  • The terminal must be connected (gRPC stream active)
  • If the terminal was offline when blocked, it will see the blocked status on next API query
  • Check onBlockCommand callback implementation