> For the complete AppRefer docs index, see https://apprefer.com/llms.txt (every page as markdown) or https://apprefer.com/llms-full.txt (all docs in one file).

# Android SDK

Native Kotlin library for attribution, product events, AppRefer Billing (Google Play Billing) and the in-app Support chat, with Java-friendly callbacks.

Every page in this section follows the same order, so you can switch between the [iOS](https://apprefer.com/docs/sdk/ios.md), [Android](https://apprefer.com/docs/sdk/android.md), [Flutter](https://apprefer.com/docs/sdk/flutter.md) and [React Native](https://apprefer.com/docs/sdk/react-native.md) references and find the same thing in the same place. All four SDKs share one version number; this page documents **0.13.1**.

## Install

The SDK is published through JitPack. Add the repository once per project, then the dependency.

**settings.gradle.kts**

```kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://jitpack.io")
    }
}
```

**app/build.gradle.kts**

```kotlin
dependencies {
    // Attribution, events and Support only (minSdk 21, no Play Billing)
    implementation("com.github.AppAgentic.apprefer-android-sdk:apprefer-android-sdk:0.13.1")

    // Or attribution + AppRefer Billing (minSdk 23, adds Play Billing 8.3)
    implementation("com.github.AppAgentic.apprefer-android-sdk:apprefer-billing:0.13.1")
}
```

The short coordinate `com.github.AppAgentic:apprefer-android-sdk:0.13.1` brings in both modules, including Play Billing and its `BILLING` permission.

> **Pre-1.0 versions**
>
> Before 1.0, a minor version can change behaviour. Pin an exact version and read the changelog highlights below before upgrading.

### Platform setup

-   **Install Referrer** is a dependency of the SDK; you don't add it yourself. AppRefer reads the Play referrer on first launch (click ids such as `gclid` and `ar_click_id`, plus `utm_*`).
-   **Permissions.** The SDK declares `android.permission.INTERNET` and `com.google.android.gms.permission.AD_ID`. The Advertising ID is read only when tracking consent is granted (or not set). If your app shouldn't declare it, remove it in your manifest; attribution then works without it:

**AndroidManifest.xml**

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="com.google.android.gms.permission.AD_ID"
        tools:node="remove" />
</manifest>
```

-   The SDK picks up your application context on its own (through a small content provider), so calls made before `configure` that need a context still work.
-   For right-to-left languages in the Support chat, keep `android:supportsRtl="true"` on your `<application>`.

## Configure

Call `configure` once at launch. On the first launch it sends the install's signals and Play Install Referrer to AppRefer and resolves attribution; on later launches it returns the cached result. It is a suspend function and never throws.

**App.kt**

```kotlin
import com.apprefer.sdk.AppRefer

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        CoroutineScope(Dispatchers.IO).launch {
            val attribution = AppRefer.configure(this@App, apiKey = "pk_live_...")
            // attribution?.network, attribution?.matchType
        }
    }
}
```

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `context` | Context | required | Any context; the application context is kept |
| `apiKey` | String | required | SDK key (`pk_live_…` or `pk_test_…`) |
| `userId` | String? | null | Your app's user id, if someone is already signed in |
| `debug` | Boolean | false | Logcat logs |
| `logLevel` | Int | 1 | 0 none, 1 errors, 2 warnings, 3 verbose |

`configure` returns `Attribution?`. Methods called before it do nothing.

### Java

Every suspend call has a callback overload taking `AppReferCallback<T>`:

```java
AppRefer.configure(this, "pk_live_...", null, false, 1,
    new AppReferCallback<Attribution>() {
        @Override public void onResult(Attribution result) { /* may be null */ }
        @Override public void onError(Throwable error) { }
    });
```

> **Live and test keys**
>
> The server picks the environment from the key. `pk_test_` installs, events and purchases are sandbox data: visible in the dashboard's sandbox view and Debugger, never forwarded to ad networks. Use `pk_live_` in release builds.

### configureResult

`configureResult` takes the same parameters and tells you why there is or isn't an attribution. It reports organic only after the server has said so, never because the network failed.

```kotlin
when (val result = AppRefer.configureResult(context, apiKey = "pk_live_...")) {
    is AppReferAttributionResult.Attributed -> showWelcome(result.attribution.campaignName)
    AppReferAttributionResult.Pending -> Unit    // decision held by the server
    AppReferAttributionResult.Organic -> Unit    // the server answered: no ad click matched
    is AppReferAttributionResult.Failed -> Log.w("app", "no answer", result.error)
}
```

| Result | Meaning |
| --- | --- |
| `Attributed(attribution)` | Matched to an ad click or a Play referrer |
| `Pending` | The server is holding the decision while consent is undetermined (iOS installs, mostly) |
| `Organic` | Confirmed organic install |
| `Failed(error)` | No answer from the server and nothing cached |

`AppRefer.getAttribution()` returns the cached attribution at any time without a network call.

## Attribution result

The `Attribution` data class returned by `configure` and `getAttribution()`:

| Property | Type | Description |
| --- | --- | --- |
| `network` | String | Ad network, e.g. `meta`, `google`, `tiktok`, `openai`, or `organic` |
| `matchType` | String | How the install was matched, e.g. `install_referrer`, `install_referrer_gclid`, `ip_match`, `probabilistic`, `organic` (see [Attribution](https://apprefer.com/docs/attribution.md)) |
| `attributionId` | String? | AppRefer's id for this attribution |
| `campaignId` | String? | Campaign id from the ad network or tracking link |
| `campaignName` | String? | Campaign name |
| `adGroupId` | String? | Ad set / ad group id |
| `adId` | String? | Ad id |
| `keyword` | String? | Search keyword |
| `fbclid` | String? | Meta click id, when matched to a Meta click |
| `gclid` | String? | Google click id |
| `ttclid` | String? | TikTok click id |
| `campaign` | String? | Older field kept for compatibility; use `campaignName` |
| `queryParams` | Map<String, String> | Extra tracking-link parameters, when present |
| `customData` | Map<String, String> | Custom data, when present |
| `trackingDecisionPending` | Boolean | True while the server waits for a consent decision |
| `createdAt` | Long | When the attribution was made (epoch ms) |

## Tracking consent

Android has no system tracking prompt, so you tell AppRefer what your app's own consent flow decided. The choice is saved across launches and may be set before `configure`.

```kotlin
AppRefer.setTrackingConsent(true)    // granted
AppRefer.setTrackingConsent(false)   // denied
AppRefer.setTrackingConsent(null)    // not set: legacy behaviour

AppRefer.trackingConsentDidChange()  // re-send after any other change
```

| Consent | Host | What AppRefer does |
| --- | --- | --- |
| `true` | `trk.apprefer.com` | Full matching, Advertising ID, advanced matching and forwarding to ad networks |
| `false` | `apprefer.com` | First-party attribution, analytics, Billing and Support; no Advertising ID, no advanced matching, nothing forwarded |
| `null` (default) | `trk.apprefer.com`, falling back to `apprefer.com` | Legacy behaviour: requests carry no consent value and are treated as allowed |

Apps that need consent recorded before any tracking can ask AppRefer to treat a missing value as denied (the app setting `trackingConsentRequired`). See [Privacy](https://apprefer.com/docs/privacy.md).

## User identity

Tell AppRefer who is signed in, so Support conversations and product events carry the account (`app_user_id`). Use the same id you pass to `AppReferBilling.logIn`. You can also pass it as `userId` to `configure`.

```kotlin
AppRefer.setUserId(currentUser.id)          // suspend
AppReferBilling.logIn(currentUser.id)       // if you use AppRefer Billing
```

### Advanced matching

Send hashed customer details to improve Meta match rates. Values are SHA-256 hashed on the device and dropped unless tracking consent is granted (or not set). Call it once after sign-up or sign-in.

```kotlin
AppRefer.setAdvancedMatching(
    email = "user@example.com",
    phone = "+15551234567",
    firstName = "Jane",
    lastName = "Doe",
    dateOfBirth = "19900131",   // YYYYMMDD
)
```

### Device id

`AppRefer.getDeviceId()` returns AppRefer's install id (`null` before `configure`). AppRefer Billing sends it with purchases automatically. If you take payments through RevenueCat instead, set it as the `appreferId` subscriber attribute (see [RevenueCat](https://apprefer.com/docs/integrations/revenuecat.md)).

## Events

Track the in-app moments that matter to you (sign-up, onboarding finished, first workout). Don't track purchases here: AppRefer Billing records purchases, renewals and refunds and forwards them for you. Which events reach ad networks is covered in [Events](https://apprefer.com/docs/events.md).

```kotlin
AppRefer.trackEvent("signup")
AppRefer.trackEvent("tutorial_complete", properties = mapOf("step" to "final"))

// Need to know what happened to it?
val result = AppRefer.trackEventResult("level_up", properties = mapOf("level" to 3))
// AppReferEventResult.SENT, QUEUED or DROPPED
```

| Parameter | Type | Description |
| --- | --- | --- |
| `eventName` | String | Event name, e.g. `signup` |
| `properties` | Map<String, Any?>? | JSON-compatible metadata |
| `revenue` | Double? | Amount for revenue events you track yourself |
| `currency` | String? | ISO 4217 code, e.g. `USD` |

### Offline queue

-   Events are saved on the device before they are sent and keep one stable event id, so delivery is **at least once**.
-   The queue holds up to 1,000 events for up to seven days; the oldest are dropped first.
-   Network errors, HTTP 408, 429 and 5xx are retried with backoff (up to an hour, or longer if the server asks), at launch and when the app returns to the foreground. Other 4xx answers are final.
-   `AppRefer.clearPendingEvents()` cancels event requests and empties the queue, for example when the user withdraws analytics consent.

## Collection control

`setCollectionEnabled(false)` stops attribution, consent reports and product events, cancels what is in flight and clears the event queue. The choice is saved across launches; if it can't be saved, collection still stops for this session. Billing and Support keep working, without the install's device id.

```kotlin
AppRefer.setCollectionEnabled(false)   // consent withdrawn (suspend; optional context)

// Later, to resume: enable, then configure again
AppRefer.setCollectionEnabled(true)
AppRefer.configure(context, apiKey = "pk_live_...")
```

Enabling alone sends nothing. Calling it with `true` while collection is already on changes nothing, so it is safe to call from a consent tool on every launch.

## Account switching

Call `reset()` when a user signs out, and wait for it before the next account signs in. It revokes the old Support session and its push token, clears the Support profile, history and drafts, the user id and queued events. The install's own attribution and device id stay.

```kotlin
// Sign out
AppReferBilling.logOut()   // if you use AppRefer Billing
AppRefer.reset()

// Next account signs in
AppRefer.configure(context, apiKey = "pk_live_...", userId = nextUser.id)
AppReferBilling.logIn(nextUser.id)
```

-   After `reset()`, attribution, user id updates, advanced matching and events are paused until you call `configure` again. Events tracked in between are dropped, never sent later.
-   Billing logout stays explicit. Until Billing's `logIn` or `logOut` runs, the Billing user who was current at reset gets no install device id, even after a relaunch (0.9.3+).
-   `reset()` is safe to call before `configure` and more than once. It doesn't change the collection setting.

## SKAdNetwork and AdAttributionKit

SKAdNetwork and AdAttributionKit are Apple-only. `AppRefer.updateConversionValue()` exists on Android for API parity and does nothing. If you ship the same app on iOS, see the [iOS SDK](https://apprefer.com/docs/sdk/ios.md#skadnetwork-and-adattributionkit) and [SKAdNetwork](https://apprefer.com/docs/integrations/skan.md).

## AppRefer Billing

`AppReferBilling` (module `apprefer-billing`) runs Google Play subscriptions and one-time products on Play Billing Library 8. AppRefer verifies each purchase with Google, acknowledges it, keeps the customer's entitlements and sends the conversion to your ad networks from the same attribution, so no separate purchases SDK or webhook is needed. Billing must be turned on for the app in AppRefer first (package name, a Play service account and products); see [Billing](https://apprefer.com/docs/billing.md) for the server setup and the move from RevenueCat.

### Set up

```kotlin
import com.apprefer.sdk.billing.AppReferBilling

// In Application.onCreate, after starting AppRefer.configure
AppReferBilling.configure(
    context = this,
    appUserId = null,   // null = anonymous "$ARAnon:<UUID>", kept on the device
    entitlementsByProduct = mapOf(
        "pro_monthly" to listOf("premium"),
        "pro_yearly" to listOf("premium"),
    ),
    consumableProductIds = setOf("coins_100"),   // one-time products that can be bought again
)
```

| Parameter | Default | Description |
| --- | --- | --- |
| `context` | required | Any context |
| `apiKey` | the configure key | SDK key, if different |
| `appUserId` | null | Your user id; `null` starts anonymous |
| `entitlementsByProduct` | empty | Product id to entitlement ids, used only when AppRefer can't be reached |
| `defaultEntitlement` | null | Entitlement for unmapped products (the server default is `premium`) |
| `consumableProductIds` | empty | One-time products consumed after AppRefer records them; others are treated as lifetime |
| `debug` | `false` | Logcat logs |
| `logLevel` | 1 | 0 to 3, as for configure |

### Offerings and paywalls

Offerings let you change what a paywall sells from the dashboard (packages, placements, targeting and experiments) without hard-coding product ids. See [Offerings](https://apprefer.com/docs/offerings.md).

```kotlin
val offerings = AppReferBilling.offerings()

val annual = offerings.currentOffering(forPlacement = "onboarding")?.annual
if (annual != null) {
    // annual.storeProduct is the Play ProductDetails; annual.offerToken the selected offer
    val result = AppReferBilling.purchase(activity, annual)
}
```

-   `offerings.current` is the resolved default and `offerings.all` holds every offering by id. Each offering has `availablePackages`, `metadata`, a package lookup and the shortcuts `weekly`, `monthly`, `twoMonth`, `threeMonth`, `sixMonth`, `annual` and `lifetime`.
-   A Play product written `subscription:basePlan` selects that base plan; a bare subscription picks the base plan matching the package type, otherwise the first.
-   An unknown placement falls back to the current offering; a placement set to no offering returns `null` (show no paywall).
-   Buying a package records the offering, placement and experiment with the purchase.
-   Offerings are cached for five minutes (memory and disk). Offline, the last copy is used; with no copy at all the call throws, so keep your existing product ids as a fallback.

### Products, purchase and restore

```kotlin
val products = AppReferBilling.products(listOf("pro_monthly", "pro_yearly"))   // Play ProductDetails, in this order

val result = AppReferBilling.purchase(activity, products[1])
when (result.outcome) {
    AppReferBilling.PurchaseResult.Outcome.PURCHASED -> unlock(result.customerInfo)
    AppReferBilling.PurchaseResult.Outcome.USER_CANCELLED -> Unit
    AppReferBilling.PurchaseResult.Outcome.PENDING -> showPendingNote()   // arrives later through customerInfoStream
}

val info = AppReferBilling.restorePurchases()   // queryPurchasesAsync, then sends every active purchase
```

-   `purchase` sets `obfuscatedAccountId` to the customer's AppRefer account token and sends the purchase to AppRefer, which verifies and acknowledges it. Consumables are consumed only after AppRefer has accepted them.
-   Subscription offers: pass `offerToken`. Without one the SDK picks a free-trial offer the user is eligible for, else the base plan (`AppReferBilling.defaultOfferToken(product)`).
-   Upgrades and downgrades: pass `oldProductId` and `replacementMode` (default `WITH_TIME_PRORATION`).
-   Pending payments (cash, bank transfer) return `PENDING`, grant nothing and are recorded once Google Play reports them paid.
-   Google Play errors throw `AppReferBillingException.Store(responseCode, debugMessage)`. If who is buying can't be saved before Play opens, `purchase` throws `AppReferBillingException.Storage` instead.
-   Only `restorePurchases()` can move a purchase owned by another app user to the one signed in. `AppReferBilling.rejectedPurchases()` lists purchases AppRefer refused for good, for your support team.

### Customer info and entitlements

```kotlin
val info = AppReferBilling.customerInfo()                                       // cached for 5 minutes, then fetched
val fresh = AppReferBilling.customerInfo(AppReferBilling.FetchPolicy.FETCH_CURRENT)

if (info.entitlements["premium"]?.isActive == true) { /* unlocked */ }

AppReferBilling.customerInfoStream.collect { update(it) }        // Flow, latest value first
AppReferBilling.listener = AppReferBillingListener { update(it) } // or a listener, on the main thread
```

| Field | Description |
| --- | --- |
| `entitlements` | By id; `.active` lists only active ones |
| `activeSubscriptions` | Product ids of active subscriptions |
| `appUserId` | The customer the answer is for |
| `source` | `SERVER`, `CACHE` or `LOCAL_FALLBACK` |
| `EntitlementInfo` | `isActive`, `productIdentifier`, `expirationDate`, `willRenew`, `periodType`, `store` |

`customerInfo` never throws. If AppRefer can't be reached, entitlements come from Google Play's own active purchases, mapped through `entitlementsByProduct`, so a paying user is never locked out. If the upload fails, the SDK acknowledges the purchase on the device as soon as its retry copy is saved, so Google never refunds it for being unacknowledged.

### Users

```kotlin
AppReferBilling.logIn("user_123")   // moves purchases made while anonymous to this user
AppReferBilling.logOut()            // new anonymous id

val id = AppReferBilling.appUserId          // "$ARAnon:..." while anonymous
val anonymous = AppReferBilling.isAnonymous

AppReferBilling.setEmail("a@example.com")   // also setDisplayName, setPhoneNumber, setAttributes
```

The install's device id is sent with purchases automatically, so purchases are matched to the ad click without `setUserId`. It is left out while collection is off and after `reset()` until `configure` runs again.

### Sandbox and App Review

-   Test with a license tester account on an internal testing track. Those purchases are recorded as sandbox, unlock the entitlement and are never forwarded to ad networks or counted as revenue.
-   Store reviewers' test purchases work with the live key.
-   A real (production) purchase sent with a `pk_test_` key is refused.
-   Play Billing needs a device or emulator image with the Play Store.

## Support

An in-app help chat your team answers from the AppRefer inbox, with screenshots, quick-help topics, reply notifications and a help centre. Native views, no extra runtime dependencies, 35 languages. Server setup and the inbox are covered in [Support](https://apprefer.com/docs/support.md).

### Open the chat

```kotlin
AppRefer.support.open()                                   // help home
AppRefer.support.open("cv_...")                            // one conversation
AppRefer.support.compose("Problem with my subscription")   // home with the message field filled in
// Context overloads: open(context, id), compose(context, text)

// Or embed it in your own layout
AppReferSupportView(context, destination = AppReferSupportDestination.Home)
// HelpCentre, Article("slug"), Conversation("id")
```

When you embed `AppReferSupportView`, wire `onClose` and `onPickImages`, pass picked URIs to `addImages`, and forward `onResume`/`onPause` to `resume`/`pause`. The full-screen activity does this for you.

### Identify and customise

```kotlin
AppRefer.support.identify(name = "Alex", email = "alex@example.com")   // the user id is sent automatically
AppRefer.support.accentColor = Color.rgb(120, 82, 255)   // text on a bright accent turns dark
AppRefer.support.title = "Sparkly support"               // default "<app label> support", translated
AppRefer.support.teamNames = listOf("Sam")               // first names on the home
AppRefer.support.replyTime = "a few hours"               // completes "We usually reply in …"
AppRefer.support.appIcon = drawable                      // default: the launcher icon
AppRefer.support.topics = listOf(
    AppReferSupportTopic.billing,
    AppReferSupportTopic.restore.copy(action = { restorePurchases() }),
    AppReferSupportTopic("Report a bug", "Screenshots welcome", R.drawable.ic_bug, Color.RED, "What went wrong?"),
)
AppRefer.support.language = "de"                         // force a language; null follows the app
AppRefer.support.strings = mapOf("support.home.subtitle" to "What can we fix for you?")
```

Quick-help tiles default to `billing`, `restore`, `bug` and `idea`; an empty list hides them. A tile with an `action` runs it instead of starting a message. The chat follows the app's language (limited to the languages in your `localeConfig`, if you declare one), then the device, then English. You can also override a string the Android way, with a resource such as `apprefer_support_home_subtitle` in your own `values-xx`.

### Reply notifications

AppRefer sends reply notifications through FCM with your service account, and emails a reply only if it stays unread for an hour. Your app owns Firebase Messaging and the notification permission prompt; the SDK adds no Firebase dependency. Register the token once `status().pushReady` is true.

```kotlin
// After getting an FCM token (suspend calls)
if (runCatching { AppRefer.support.status() }.getOrNull()?.pushReady == true) {
    AppRefer.support.registerPushToken(token)
}
AppRefer.support.removePushToken(token)   // when unregistering this device

// From your notification tap handler (not on background receipt)
if (AppRefer.support.handleNotification(data)) return

val unread = AppRefer.support.unreadCount()   // for a badge
```

Replies arrive on the `apprefer_support` channel (the SDK creates it, named in the chat's language) with data `{type: "support_reply", conversationId}`. Call `registerPushToken` again when the token rotates. **Push environment (0.10.1).** FCM has no sandbox and production split, so there is nothing to set on Android.

### Help centre

Published help articles appear in the chat with no code (0.10.0+): a "Help articles" card on the home, a searchable help centre and a reader. You can open them directly or build your own screens.

```kotlin
AppRefer.support.openHelpCentre()
AppRefer.support.openArticle("restore-purchases")

val articles = AppRefer.support.articles()           // List<AppReferHelpArticle>
val results = AppRefer.support.articles("refund")    // server search, up to 20
try {
    val article = AppRefer.support.article("refunds") // + body (markdown)
} catch (e: AppReferSupportException) {
    if (e.isNotFound) { /* removed or unpublished */ }
}
```

Articles come in the chat's language and are cached for five minutes; offline, the last copy is shown. Test keys also see drafts.

### Status

```kotlin
val status = AppRefer.support.status()
status.pushReady            // FCM configured and Google accepted the service account
status.emailReplies         // customers can answer reply emails
status.helpCentreArticles   // number of published articles
```

`status()` follows the server's caching (five minutes once every channel is verified, otherwise 30 seconds), so calling it at every launch is cheap.

### Support without attribution

Apps that want only the chat can start Support on its own. It doesn't configure attribution, collect device signals or send analytics.

```kotlin
AppRefer.support.configure(context, apiKey = "pk_live_...")

// Forget the name, email and user id and start a fresh anonymous session (suspend)
AppRefer.support.clearIdentity()
```

## Requirements

| Requirement | Minimum |
| --- | --- |
| minSdk | 21 (Android 5.0); 23 with apprefer-billing |
| compileSdk | 35 |
| JDK | 17 |
| Kotlin | 2.0; 2.1 with apprefer-billing |
| Dependencies | kotlinx-coroutines, Install Referrer, play-services-ads-identifier; Play Billing 8.3 with apprefer-billing |

## Changelog highlights

| Version | What changed |
| --- | --- |
| 0.13.1 | Narrow-screen Support text wrapping, measured composer spacing and keyboard anchoring. React Native Support-only apps bundle without the optional purchase package. |
| 0.13.0 | Photo/video testimonials from Support, the app or an authenticated coach; usage terms accepted on Submit, signed uploads and receipt-based rewards. See the Support reference for integration. |
| 0.12.0 | Verified customer context, resolution actions, offering and optional review cards, outcomes and tailored Support setup. |
| 0.11.0 | Version parity release. Nothing on this page changes. |
| 0.10.1 | Version parity with the iOS push-environment change; no Android behaviour change. |
| 0.10.0 | Help centre in the chat; `support.articles`, `article`, `status()`, `openHelpCentre()`, `openArticle()`. Chat in 35 languages with `language` and `strings` overrides. No API 24+ collection calls on older devices. |
| 0.9.3 | The Billing account gate set by `reset()` survives a relaunch. |
| 0.9.2 | Billing sends no install device id while collection is off or after `reset()`; new `support.clearIdentity()`. |
| 0.9.1 | Events carry `app_user_id`; `setCollectionEnabled`; after `reset()` call `configure` again. |
| 0.9.0 | `reset()`, Support-only configure, durable event queue, `trackEventResult`, `clearPendingEvents`, `configureResult`. |
| 0.8.0 | Offerings, placements and experiments; `purchase(activity, package)`. Products mapped to no entitlement unlock nothing offline. |
| 0.7.0 | `updateConversionValue()` added as a no-op for parity with iOS. |
| 0.6.0 | Tracking consent split: `setTrackingConsent`, `trackingConsentDidChange`; requests pick their host from consent. |

Full history: the `CHANGELOG.md` in [apprefer-android-sdk](https://github.com/AppAgentic/apprefer-android-sdk).

---

Source: https://apprefer.com/docs/sdk/android · Last updated: 2026-10-20
