central.sonatype.com ↗
Installimplementation("io.signalbird:signalbird-sdk:2.10.1")

signalbird-sdk - Kotlin (Android)

It carries two clients:

Class What for Key Where
SignalbirdApp live chat + push registration for the end user sb_public_live_… (public) the Android app
SignalbirdClient Radio - writing logs and events sb_secret_live_… (secret) server-side Kotlin (Ktor, Spring)

A secret key is never embedded in the APK: the package can be unpacked and the key ends up in everyone's hands. The mobile side does chat and push registration with the public key.

Install

kotlin
// build.gradle.kts
dependencies {
    implementation("io.signalbird:signalbird-sdk:1.9.0")
}
xml
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />

The only external dependency is coroutines, which every Android project already has. For JSON it uses Android's built-in org.json: OkHttp, Retrofit or Moshi are not forced on you, because an SDK choosing the customer's networking and serialization stack is the most common source of version conflicts.


Storing the visitor secret

Supplying a storage is mandatory. The default is an in-memory store meant only for tests: if the secret does not survive on the device, the user loses their chat history every time they open the app.

kotlin
class PrefsStorage(context: Context) : SignalbirdStorage {
    private val prefs = context.getSharedPreferences("signalbird", Context.MODE_PRIVATE)

    override fun get(key: String): String? = prefs.getString(key, null)
    override fun set(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    override fun remove(key: String) { prefs.edit().remove(key).apply() }
}

The stored record carries the app key too: if you rotate the key, the old identity is discarded automatically and a new session opens.


Live chat

kotlin
import io.signalbird.sdk.*

val client = SignalbirdApp(SignalbirdAppConfig(
    publicKey = "sb_public_live_…",
    chatKey = "destek",
    pushKey = "mobil",
    locale = "en",
    storage = PrefsStorage(applicationContext),
))

chatKey is the chat channel's module key and pushKey the push channel's (the channel names you create in the panel; not secret). The SDK sends pushKey on device and push endpoints (registerDevice, unregisterDevice, reportPushOpened) and chatKey on chat endpoints, as the X-Signalbird-Module-Key header; if the relevant key is missing the API returns 400 MODULE_KEY_MISSING (2.10.0).

You get the app key from the panel (Inbox → Settings → Apps) or by calling createApp with the Management SDK.

Bootstrap

kotlin
lifecycleScope.launch {
    val boot = client.bootstrap()
    val app = boot.json?.optJSONObject("app")

    if (app?.optBoolean("chat_enabled") != true) return@launch   // chat is off
}

Session and conversation

kotlin
lifecycleScope.launch {
    client.startSession(mapOf("name" to "Ayşe", "email" to "ayse@example.com"))

    val started = client.startConversation("Where is my parcel?")
    val conversationId = started.json
        ?.optJSONObject("conversation")
        ?.optString("id") ?: return@launch

    client.sendMessage(conversationId, "Order number: 1234")
}

Fetching new messages

kotlin
val updates = client.getConversation(conversationId, after = lastMessageId)

There is no WebSocket. Poll every 3 seconds while the screen is in front and on a gradually widening interval in the background, skipping the round while the app is backgrounded: this recovers by itself when the connection drops and does not burn battery.

kotlin
// Polling bound to the lifecycle
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.RESUMED) {
        while (isActive) {
            client.getConversation(conversationId, after = lastMessageId)
            delay(3_000)
        }
    }
}

Interactions

kotlin
client.setTyping(conversationId, true)
client.markRead(conversationId, lastMessageId)
client.reactToMessage(conversationId, messageId, "👍")

// Only their own message, and only within 15 minutes of sending it
client.editMessage(conversationId, messageId, "Correction")
client.deleteMessage(conversationId, messageId)

client.rateConversation(conversationId, rating = 5, comment = "Thank you")
client.closeConversation(conversationId)

Identity

kotlin
client.identify(mapOf(
    "external_id" to user.id,
    "email" to user.email,
    "name" to user.displayName,
    "attributes" to mapOf("plan" to "pro"),
))

client.signOut()   // clears the local identity; the record on the server stays

Push (FCM)

kotlin
class SignalbirdMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        CoroutineScope(Dispatchers.IO).launch {
            client.registerDevice(
                token = token,
                externalId = currentUser?.id,
                deviceName = Build.MODEL,
                appVersion = BuildConfig.VERSION_NAME,
            )
        }
    }
}

Asking for the Android 13+ notification permission (POST_NOTIFICATIONS) and obtaining the token is the app's job: when you ask is a product decision. The SDK only forwards the token.

On sign-out:

kotlin
client.unregisterDevice(token)   // the record is deactivated, not deleted

Radio (server-side Kotlin)

kotlin
val radio = SignalbirdClient(SignalbirdConfig(
    domainKey = System.getenv("SIGNALBIRD_DOMAIN_KEY"),
    source = "api-01",
))

radio.critical("critical", "the payment service is not responding", mapOf(
    "service" to "iyzico",
))

radio.batch(listOf(
    mapOf("channel" to "deploy", "message" to "v2.4.0 is live", "level" to "info"),
))

Five levels: SignalbirdLevel.DEBUG … CRITICAL.


Result shape

kotlin
data class SbResult(
    val ok: Boolean,
    val status: Int,
    val data: Any?,          // JSONObject, JSONArray or null
    val code: String?,
    val message: String?,
) {
    val json: JSONObject?
    val array: JSONArray?
}
kotlin
val result = client.listConversations()

result.json?.optJSONArray("data")?.let { list ->
    for (i in 0 until list.length()) {
        val conversation = list.getJSONObject(i)
        // …
    }
}

Version notice

The SDK reports its version on every request: X-Signalbird-Sdk: kotlin/<version>. When a newer version is released the API responds with Signalbird-Sdk-Status: outdated (or unsupported once it is no longer supported) and the SDK writes a warning once per process (System.err, logcat on Android). The warning is not an error: the request result does not change.

In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update, bump io.signalbird:signalbird-sdk in build.gradle.kts.

Error codes

Code Meaning
WRONG_KEY_TYPE The constructor throws IllegalArgumentException
VISITOR_INVALID The secret is invalid; a new session must be opened
CHAT_UNAVAILABLE The quota is full or chat is off
MODULE_DISABLED Your package does not include the chat or push module
NETWORK_ERROR / TIMEOUT Network (status 0)

Notes

  • Every method is suspend and runs on Dispatchers.IO; you do not have to manage a thread on top of that.
  • PATCH requests go out as POST plus an X-HTTP-Method-Override header: Android's HttpURLConnection does not know the PATCH method. On the server side there is no difference.
  • Apart from the key check in the constructor, no method throws; the result is always the envelope.