> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Android SDK: Orbit quickstart for Kotlin

> Official Orbit Android SDK quickstart — in-app chat, in-app messages and content cards, and video rooms for Kotlin and Jetpack Compose.

# Android SDK (Kotlin)

The Orbit Android SDK embeds Orbit's end-user-facing surfaces into a native Android app: in-app chat (the same omnichannel conversation/inbox the web widget and iOS SDK use), owned in-app messages and content cards, and video rooms. It ships as a single Gradle module with three clients — `OrbitChatClient`, `OrbitInAppClient`, and `OrbitVideoRoomClient` — and targets Android API 21+ (Kotlin, coroutines, kotlinx.serialization, OkHttp).

This is a **client SDK**: it authenticates only with a publishable key (`dv_live_pk_…`), because it ships inside a distributed app binary. It intentionally does not wrap the server-side management resources (messaging, voice, contacts, campaigns, verify) — those require a secret key and belong in your backend, via the [Node SDK](/sdks/node) or the REST API. See [Client / mobile SDKs (different scope, by design)](/sdks) on the SDK index.

<Note>
  **Pre-publish — source-only.** The Android SDK is not yet on Maven
  Central — the Gradle dependency line below fails today. Until first
  publish, vendor the source from the monorepo (`packages/sdk-android/`)
  and include it as a Gradle project dependency.
</Note>

## Installation (Gradle)

```kotlin theme={null}
// app/build.gradle.kts
dependencies {
    implementation("io.devotel:orbit-chat-sdk:0.1.0")
}
```

## Headless chat client (OrbitChatClient)

The client ships zero UI — it owns the conversation lifecycle, history, optimistic send, and the real-time inbound stream; your Activity, Fragment, or Compose screen renders it. All network methods are `suspend` functions.

```kotlin theme={null}
import io.orbit.devotel.chat.OrbitChatClient
import io.orbit.devotel.chat.OrbitChatConfig
import io.orbit.devotel.chat.SharedPreferencesStorage

val chat = OrbitChatClient(
    OrbitChatConfig(
        publicKey = "dv_live_pk_…",                    // publishable key ONLY — never a secret key
        storage = SharedPreferencesStorage(context),   // persist across launches
        appInfo = "acme-android/2.3.1",
    )
)

chat.onMessage { message -> println("agent: ${message.body}") }
chat.onMessages { all -> render(all) }
chat.onTyping { isTyping -> setTyping(isTyping) }
chat.onConnection { status -> println("status: $status") }

chat.connect()
chat.sendMessage("Hi, I need help with my order")
```

Each `on…` method returns an `OrbitSubscription`; call `.cancel()` on it to stop receiving the event. Call `chat.disconnect()` to stop the real-time stream, and `chat.close()` when the client is done (it cancels the internal coroutine scope — the client cannot be reused after that).

Send a file attachment (10 MB client-side guard):

```kotlin theme={null}
val bytes = contentResolver.openInputStream(uri)!!.readBytes()
chat.sendAttachment(
    OrbitChatAttachment(
        bytes = bytes,
        name = "photo.jpg",
        mimeType = "image/jpeg",
        size = bytes.size,
    )
)
```

## In-app messages and content cards (OrbitInAppClient)

Owned in-app surfaces — assembled server-side into a per-visitor feed, rendered locally, with impression / click / dismiss reported back. Dismissals persist across launches through the storage adapter.

```kotlin theme={null}
import io.orbit.devotel.inapp.OrbitInAppClient
import io.orbit.devotel.inapp.OrbitInAppConfig

val inApp = OrbitInAppClient(
    OrbitInAppConfig(publicKey = "dv_live_pk_…")
)

val feed = inApp.fetchFeed()
inApp.reportEvent("impression", feed.cards.first().id)
inApp.dismiss(feed.cards.first().id)
```

## Video rooms (OrbitVideoRoomClient)

Headless and engine-injected: your backend mints the room token server-side (`POST /api/v1/video/rooms/:id/join`) and hands the token plus server URL to the app. Wire the client over a `VideoRoomEngine` backed by a WebRTC engine (for example the Orbit Media SDK for Android), so this package carries no hard WebRTC dependency.

```kotlin theme={null}
import io.orbit.devotel.video.OrbitVideoRoomClient
import io.orbit.devotel.video.OrbitVideoRoomConfig

val room = OrbitVideoRoomClient(
    OrbitVideoRoomConfig(token = roomToken, serverUrl = roomServerUrl),
    engine = myVideoRoomEngine,
)

room.onTrackSubscribed { track, participant -> render(track, participant) }
room.onCaption { caption -> showCaption(caption) }
room.join()

val micOn = room.toggleMicrophone()
room.joinBreakout(name = "support-tier-2", token = breakoutToken)
room.leave()
```

## Error handling

API failures surface as a typed `OrbitApiError` carrying the HTTP status and the server response excerpt; a parsed `Retry-After` lets a 429 be throttled instead of retried in a hot loop. Construction misuse (empty key or a server-side secret key) throws `IllegalArgumentException`; attachment-size checks throw `OrbitChatException`.

```kotlin theme={null}
import io.orbit.devotel.chat.OrbitApiError

try {
    chat.sendMessage("Where is my order?")
} catch (err: OrbitApiError) {
    when (err.status) {
        429 -> // respect the Retry-After hint before retrying
        401 -> // the publishable key is no longer valid
        else -> println("orbit error: HTTP ${err.status}")
    }
}
```

Only the **publishable key** (`dv_live_pk_…` / `dv_test_pk_…`) may be embedded in the app. The clients refuse a server-side secret key (`dv_live_sk_…` / `dv_test_sk_…`) at construction, because anything bundled in a distributed binary is extractable by any user. Real-time updates use header-authenticated polling, so the key never lands in a URL log.
