<!-- AI agents: this is the Markdown version of a Reception documentation page. Index: /llms.txt -->

# Android

Connect Reception to a native Android app built with Jetpack Compose or Views. The [setup prompt](/docs/get-started#setup-prompt) guides placement, user details and the design round; this page supplies its Kotlin code. For JavaScript apps, use [React Native](/docs/react-native); for Dart apps, use [Flutter](/docs/flutter). Both packages add Android with their next releases: `reception_flutter` 1.2.0 and `react-native-reception` 1.6.0.

Requires Android 7.0 (API 24) or later, `compileSdk` 35 or later, Kotlin 2.0 or later and Android Gradle Plugin 8. The SDK draws its chat with Jetpack Compose and Material 3; View-based apps use it without writing Compose code. Android SDK 1.0.0 has the features of iOS SDK 1.7.0.

## 1. Install {#install}

Add the dependency to the app module’s `build.gradle.kts`. The SDK is on Maven Central, which Android projects list in `settings.gradle.kts` by default:

```kotlin app/build.gradle.kts
dependencies {
    implementation("sh.reception:reception:1.0.0")
}
```

With a version catalog, add `reception = { module = "sh.reception:reception", version = "1.0.0" }` under `[libraries]` and use `implementation(libs.reception)`. In a Groovy `build.gradle`, write `implementation 'sh.reception:reception:1.0.0'`. Sync Gradle afterwards.

The SDK needs no manifest edits, no ProGuard or R8 rules and no other dependency. It uses Firebase Cloud Messaging and Google Play Billing only when your app already includes them. Its manifest adds the `INTERNET`, `ACCESS_NETWORK_STATE` and `POST_NOTIFICATIONS` permissions to your app; the notification permission is requested only by the [chat’s permission card](/docs/push-notifications#chat-prompt), if you turn it on.

## 2. Configure {#configure}

Configure once in your `Application.onCreate`, before any activity uses Reception. Keep your other startup work:

```kotlin MyApplication.kt
import android.app.Application
import sh.reception.Reception

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Reception support chat. Docs for changes: https://reception.sh/llms.txt
        Reception.configure(this, "app_YOUR_APP_ID")
    }
}
```

If your app has no `Application` subclass yet, add one like this and register it with `android:name=".MyApplication"` on the `<application>` element in `AndroidManifest.xml`. `configure` keeps only the application context and does not wait for the network. Calling it again with the same App ID does nothing.

Call every `Reception` member from the main thread; a call from another thread is posted there. Java code reaches the same members as static methods, for example `Reception.configure(this, "app_YOUR_APP_ID")`.

## 3. Open chat {#open-chat}

Copy your app’s existing button style and call `Reception.openChat()`:

```kotlin
import androidx.compose.material3.TextButton
import androidx.compose.material3.Text
import sh.reception.Reception

// Inside your composable; use your own button styling.
TextButton(onClick = { Reception.openChat() }) { Text("Support") }
```

In a View-based screen, call it from a click listener: `supportButton.setOnClickListener { Reception.openChat() }`.

`openChat()` shows the chat as a bottom sheet over the current activity. The sheet closes by dragging, tapping outside and Back. `Reception.closeChat()` dismisses it. Your app owns the Support entry and its [unread badge](#entry).

## Appearance and language {#appearance}

Immediately after configuration, set `Reception.appearance.accentColor`, `onAccentColor`, `chatBackground`, `incomingBackground`, `incomingForeground` and `fontFamily` from matching values of your app’s theme. Leave unmatched options unset. Without an accent color, the SDK’s sheet uses the device’s dynamic color on Android 12 and later, and blue below.

Each color is a `ReceptionColor`. Pass the light and dark values of your theme as a pair, so the chat follows light and dark mode by itself:

```kotlin MyApplication.kt
// After Reception.configure. LightColorScheme and DarkColorScheme are your theme's color schemes
// (Theme.kt in Android Studio's template; make them internal instead of private).
Reception.appearance.accentColor = ReceptionColor(light = LightColorScheme.primary, dark = DarkColorScheme.primary)
Reception.appearance.onAccentColor = ReceptionColor(light = LightColorScheme.onPrimary, dark = DarkColorScheme.onPrimary)
Reception.appearance.chatBackground = ReceptionColor(light = LightColorScheme.surface, dark = DarkColorScheme.surface)
```

`ReceptionColor(color)` uses one color in both modes. If your theme’s colors are color resources with a `values-night` variant, use `ReceptionColor.resource(R.color.brand)`; the chat uses the night variant in dark mode. Java code passes ARGB values: `ReceptionColor.argb(0xFF5B4FE0, 0xFFB0A8FF)`.

For a fixed or app-selected light/dark mode, set `appearance.preferredColorScheme` to `Reception.ColorScheme.LIGHT` or `DARK`; `null` follows the system. For a custom font, build a Compose `FontFamily` from your bundled fonts, for example `FontFamily(Font(R.font.inter_regular), Font(R.font.inter_bold, FontWeight.Bold))`.

The chat follows the app’s language: its per-app language when one is set (Android 13+, or AppCompat’s `setApplicationLocales`), otherwise the phone’s language. On Android 13 and later, declare your app’s languages with `android:localeConfig` so an English-only app keeps an English chat on a German phone. Only with an in-app language picker of your own, set `Reception.languageOverride` to its language tag at launch and whenever it changes. Keep the built-in texts until the design round. See [Appearance](/docs/appearance#android) and [Language](/docs/language#android).

## Entry and unread badges {#entry}

`Reception.unreadCount` and `Reception.isChatOpen` are `StateFlow`s. In Compose, collect the count inside your existing button and hide the badge at zero:

```kotlin
val unread by Reception.unreadCount.collectAsState()
BadgedBox(badge = { if (unread > 0) Badge { Text("$unread") } }) {
    // Your existing Support button or icon.
}
```

In a Kotlin View-based screen, collect it while the screen is started:

```kotlin
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        Reception.unreadCount.collect { unread ->
            badge.isVisible = unread > 0
            badge.text = unread.toString()
        }
    }
}
```

Java code adds a listener instead, which receives the current count at once and every change on the main thread: `Reception.addUnreadCountListener(listener)` in `onStart`, `Reception.removeUnreadCountListener(listener)` in `onStop`; see [Unread messages](/docs/unread-messages#android).

Apply the app’s badge style and the setup prompt’s placement rules. Badge only the tab with the main entry, for example with `BadgedBox` on that `NavigationBarItem` icon, or `getOrCreateBadge` on a `BottomNavigationView` item (remove it at zero). More placements: [Open & close chat](/docs/entry-point#android) and [Unread badges](/docs/unread-messages#android).

## Accounts and user details {#identity}

Call these after configuration, using values from your app:

```kotlin
// A name without an account: userId is null.
Reception.identify(userId = userId, name = name, email = email)
Reception.setMetadata(metadata) // Complete Map<String, String>, at most 20 fields.
```

Use `null` for unavailable identity fields. Send entered names even in apps without accounts. Update identity and the complete metadata map whenever they change; names and emails belong in `identify`, never metadata.

When an actual signed-in session ends (including an expired restored session or account switch), call `Reception.logout()` before the next identity. Never log out a guest at startup. For deletion, call `Reception.deleteData()` from a coroutine before discarding the session; catch `ReceptionError`, keep the session and offer retry on failure:

```kotlin
lifecycleScope.launch {
    try {
        Reception.deleteData()
        // Continue your account-deletion flow.
    } catch (error: ReceptionError) {
        // Keep the session; show your error UI and offer retry.
    }
}
```

Java and callback code uses `Reception.deleteData { error -> … }`, which receives `null` on success. Both `logout()` and `deleteData()` clear identity, metadata and push token; supply them again for the next session. The SDK registers the push token again by itself.

For verified identity, use `Reception.identifyWithToken(token)` with your server’s signed token. Handle `ReceptionEvent.IdentityRejected` in `Reception.onEvent` through the app’s token-refresh flow. Follow [Users & accounts](/docs/accounts#pass-token) for the server setup, the Android token example, refresh, account switching and deletion limits.

## Push {#push}

Reception delivers Android notifications through Firebase Cloud Messaging (FCM). Your app needs Firebase Messaging; the SDK does the rest. Wire push even if permission has not been granted, preserving the app’s existing consent flow. Never request permission here.

**If your app does not use Firebase yet**, add your Android app to a Firebase project (Firebase console → Project settings → Add app, with the app’s `applicationId`), download `google-services.json` into the app module, and add the Google services plugin and Firebase Messaging:

```kotlin app/build.gradle.kts
plugins {
    // Keep your existing plugins.
    id("com.google.gms.google-services")
}

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    implementation("com.google.firebase:firebase-messaging")
}
```

Declare the plugin in the root `build.gradle.kts` with `id("com.google.gms.google-services") version "4.4.2" apply false`. Then let the SDK’s ready-made service receive the messages; add this inside `<application>` in `AndroidManifest.xml`:

```xml AndroidManifest.xml
<service
    android:name="sh.reception.ReceptionMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
```

**If your app already has a `FirebaseMessagingService`**, do not add the service above: only one service receives FCM messages. Forward Reception’s messages from your own service instead:

```kotlin
class MyMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        if (Reception.handlePushMessage(this, message.data, message.messageId)) return
        // Keep your existing handling for other messages here.
    }

    override fun onNewToken(token: String) {
        Reception.setPushToken(token)
        // Keep your existing token handling here.
    }
}
```

`handlePushMessage` shows Reception’s notification (none while the chat is open), refreshes the unread count and returns `true`; other messages return `false` and stay yours. It may be called from FCM’s worker thread. A message that FCM delivers twice shows one notification: the SDK recognizes the repeated FCM message ID, which is why you pass `message.messageId` (Firebase removes it from `message.data`).

With Firebase in the app, the SDK fetches the FCM token and registers it at configure and every time the app returns to the foreground, when notifications are allowed. Call `Reception.registerForPushIfAuthorized()` after your existing permission flow succeeds; it never asks for permission. Forward the FCM token, never an APNs token or OneSignal ID. Taps on Reception’s notifications bring back your app as the person left it, or start it, and open the chat, with no code in your app.

The SDK posts its notifications on its own channel, “Support replies”, in the person’s language. Set `Reception.notificationSmallIcon` to your app’s monochrome notification icon (for example `R.drawable.ic_notification`) at launch; without it, the SDK draws a bell. If the app sets its own launcher badge counts, set `Reception.updatesAppBadge = false` at launch.

Upload your Firebase service-account key in [Push notifications](/docs/push-notifications#dashboard-fcm), then verify delivery on a device or emulator with Google Play. With **Ask for push notifications** on in the dashboard, the SDK can also ask through a [card in the chat](/docs/push-notifications#chat-prompt) on Android 13 and later. The card appears only while remote appearance is on (the default); with `Reception.usesRemoteAppearance = false` the chat shows no card. See [Push on Android](/docs/push-notifications#android) for receipt, taps and badges in detail.

## Paywalls {#paywalls}

Register your app’s purchase screens and present them from `Reception.onPaywall`. The sheet is its own activity, so a paywall your app shows as a dialog or another activity appears under it: close the chat first.

```kotlin
Reception.paywalls = listOf(ReceptionPaywall(id = "plans", title = "Plans"))
Reception.onPaywall = { id ->
    Reception.closeChat() // The paywall then shows above, not under, the chat.
    showYourPaywall(id)
}
```

`showYourPaywall` is your app’s function. An embedded `ReceptionChatView` is part of your screen and needs no `closeChat()`. See [Paywall cards on Android](/docs/paywalls#android) for availability checks and subscription changes.

## Screens and links {#screens}

[Link cards](/docs/links) open websites, other apps, Google Play, mail and the phone without code. To let them open screens of your app, list the screens once after configuration:

```kotlin
Reception.screens = listOf(
    ReceptionScreen("settings") { navController.navigate("settings") },
    ReceptionScreen("order", value = "Order number") { number -> navController.navigate("order/$number") },
)
```

The SDK closes its sheet, then runs the screen on the main thread. Web links open in the app that claims them, otherwise in a Custom Tab over the chat; set `Reception.opensWebLinksInApp = false` to hand them to the browser. See [Link cards](/docs/links#android).

## Automations {#automations}

Report a moment for [automations](/docs/automations#track) with `Reception.track("paywall_declined", opensChat = true)`. It returns at once and does nothing offline or without a listening automation. With Google Play Billing in the app, the SDK reports whether the person has an active purchase; see [Subscription states](/docs/automations#store-events).

## Embedded chat {#embedded}

Place `ReceptionChatView()` in your own screen after configuration. Your screen owns dismissal; do not also call `openChat()`:

```kotlin
import sh.reception.ReceptionChatView

// In your own destination, under your app's MaterialTheme.
Scaffold(topBar = { /* Your existing top bar. */ }) { padding ->
    // The chat applies the keyboard and navigation-bar insets itself.
    ReceptionChatView(Modifier.fillMaxSize().padding(top = padding.calculateTopPadding()))
}
```

The embedded chat follows the enclosing `MaterialTheme`: its primary color is the accent and its background decides light or dark mode, unless you set those options. It keeps its composer above the keyboard and the navigation bar in edge-to-edge activities (`enableEdgeToEdge()`, the default from Android 15), so pass only the top padding of your `Scaffold`. View-based screens use the `sh.reception.ReceptionChatView` View, in XML or with `ReceptionChatView(context)`. See [Custom navigation](/docs/entry-point#custom-navigation).

## Events and diagnostics {#events}

Assign `Reception.onEvent` for chat, message, image, review, paywall, link, deletion and identity-rejection events:

```kotlin
Reception.onEvent = { event ->
    when (event) {
        is ReceptionEvent.IdentityRejected ->
            if (event.error.code == "identity_token_expired") {
                // Fetch a fresh token for the same signed-in account.
            }
        else -> Unit // Your existing analytics, if any.
    }
}
```

Set `Reception.logLevel = Reception.LogLevel.DEBUG` while diagnosing an integration; the SDK logs to Logcat with the tag `Reception`. See the [Kotlin API reference](/docs/sdk-reference#android) for the complete API.

## Build and emulator checks {#emulator-checks}

Build and install a debug build on an emulator or a connected phone, for example with `./gradlew :app:installDebug`. Then run the setup check: on a debuggable build, launching the app with the extra `ReceptionSetupCheck` sends “Hello from setup” once and logs the result. Replace the package and launcher activity with your app’s:

```sh
adb shell am start -S -n com.example.app/.MainActivity --es ReceptionSetupCheck app_YOUR_APP_ID
adb logcat -d -s Reception | grep "Setup check"
```

Repeat the log command until `Setup check passed` or `Setup check failed: <reason>` appears. The reasons are the same as on iOS: `app_id_mismatch`, `send_not_allowed`, `not_delivered`, `timeout` and `app_not_active`. The check needs `configure` in `Application.onCreate`. The `-ReceptionPreview` screens of the iOS SDK are not available on Android.

## Updating {#updating}

Raise the version in the dependency, sync Gradle and rebuild. The Android SDK has its own version numbers; `Reception.version` reports it. The [changelog](/changelog) lists what each release changes.
