Built on Play Billing v9. Purchases are validated server-side, so entitlements remain trustworthy independent of the client.
Android SDK
Add in-app purchases and paywalls to your Android app with Kotlin and Play Billing.
The CashSDK Android SDK wraps Play Billing in idiomatic Kotlin. Its suspend
functions plug straight into coroutines, so fetching offerings, running
purchases, and reading entitlements are all ordinary await-style calls.
Install#
Add Maven Central, then the dependency:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
implementation("com.cashsdk:cashsdk-android:1.2.0")
implementation("com.android.billingclient:billing-ktx:9.1.0")
}
Requires minSdk 24, compileSdk 35, Kotlin 2.3 and a Java 17 toolchain.
com.cashsdk:cashsdk-android is the production Maven Central coordinate. JitPack is
retained only as a fallback for older releases and is not required for 1.2.0.
Prefer to vendor the source instead? Clone the repo and include it as a module:
git clone https://github.com/cashsdk/cashsdk-android.git
include(":cashsdk-android")
project(":cashsdk-android").projectDir = file("/absolute/path/to/cashsdk-android")
dependencies {
implementation(project(":cashsdk-android"))
}
Configure#
Initialize CashSDK once in your Application.onCreate with your publishable
key. This guarantees it is ready before any activity runs.
import android.app.Application
import com.cashsdk.CashSDK
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
CashSDK.configure(context = this, publishableKey = "csk_pk_…")
}
}
Register the class in your manifest:
<application
android:name=".MyApp"
... >
Ship only publishable keys (csk_pk_… / csk_pk_…) in your app. Secret keys
(csk_sk_…) belong on your backend.
Purchase#
purchase is a suspend function that takes an Activity, because Play Billing
launches its flow from one. CashSDK drives the billing flow, verifies the
purchase token server-side, settles it (consuming a consumable, acknowledging
everything else), and returns the customer's updated entitlements.
lifecycleScope.launch {
val entitlements = CashSDK.shared.purchase(activity, "pro_monthly")
if (entitlements.isActive("pro")) {
unlockPro()
}
}
Do not call acknowledgePurchase or consumePurchase yourself. The SDK
decides which is correct from the verified product type. Getting it wrong
means Google auto-refunds the purchase after 3 days, or a consumable SKU
becomes permanently unbuyable.
Check entitlements#
entitlements is a synchronous, offline-valid snapshot; entitlementUpdates is
a Flow carrying renewals, refunds and cross-device changes.
if (CashSDK.shared.entitlements.isActive("pro")) {
// Show premium content
} else {
// Show the paywall
}
lifecycleScope.launch {
CashSDK.shared.entitlementUpdates.collect { ents -> render(ents) }
}
Restore purchases#
Let customers recover access on a new device or after reinstalling.
CashSDK.shared.restore()
if (CashSDK.shared.entitlements.isActive("pro")) {
unlockPro()
}
Identify the customer#
Link the SDK to your own user ID so entitlements sync across devices and
platforms. Call identify on every launch once the session is known, not
only at sign-in. The userToken is a short-lived token minted by your backend,
and is the only identity production trusts.
CashSDK.shared.identify(userId = "8841", userToken = tokenFromYourBackend)
// On sign-out:
CashSDK.shared.logout()
Pass un-padded numeric IDs. Attribution derives from the value of a
numeric ID, so "7", "07" and "007" produce the same token, so a
zero-padded backend will merge two customers onto one account.
Testing, and the error you will probably hit first#
Android has no offline test mode: a purchase must come from Google Play. Use a licence tester account with the app published to a track (internal testing counts). Those purchases are real, Google-signed, and free, so they exercise the entire path including verification and webhooks.
Before that is set up, the SDK will throw CashSDKError.Billing with response code 4. It
now explains itself rather than handing you the number:
That product is not available to this account. This is almost always setup, not code: the product must be ACTIVE in Play Console, the app must be published to a track (internal testing counts), the installed build must be signed with the same key as the uploaded one, and the signed-in Google account must be a licence tester. It can also take a few hours after a first upload for products to become purchasable. (Play Billing code 4)
responseCode and debug are still on the error, so when (e.responseCode) keeps working.
Code 3 (BILLING_UNAVAILABLE) usually means the build was sideloaded rather than
installed from Play, or the Play Store is signed out. Code 7 means the account already
owns the product. Call CashSDK.shared.syncPurchases() to pick it up instead of purchasing
again.
ProGuard / R8#
The published artifact ships with consumer ProGuard rules, so entitlement and result models survive R8 shrinking without configuration. If you run an aggressive custom setup, keep the SDK's public types:
-keep class com.cashsdk.** { *; }