Kotlin SDK

Configure the Voidhash Android SDK and look up its client options and methods.

The Kotlin SDK adds entitlements, feature flags, analytics, and observer-mode transaction reporting to native Android apps. Use this page to create the client, set its options, and find the methods you need. Each feature also has its own guide, linked from the section that introduces it.

The initial release is observer-only

SDK-started purchases and hosted paywalls are temporarily unavailable. Google Play transactions are still observed and submitted to Voidhash for revenue analytics, but the SDK never acknowledges or consumes them.

Compatibility

The SDK has the following requirements.

  • minSdk 23.
  • compileSdk 35. Play Billing 9 requires it of your app as well.
  • AGP 8.9.0, Kotlin 2.0.21, and Java 8 bytecode.
  • Play Billing 9.1.0.

Two Gradle modules ship in @voidhash/android. :sdk (com.voidhash.sdk) is the public SDK you integrate against. :core (com.voidhash.core) is the shared engine it depends on.

Create the client

Create one client and keep it for the lifetime of the app.

import com.voidhash.sdk.Voidhash
import com.voidhash.sdk.VoidhashOptions

val voidhash = Voidhash.configure(
    context = applicationContext,
    publishableKey = "vh_pk_...",
)

The publishable key is safe to include in the app. Never ship vh_sk_... secret keys.

There is no schema argument, because the schema lives on the server and initialize() fetches it. initialize() connects to Google Play, resolves the schema, reconciles unfinished store transactions, and starts the analytics queue. Call it once the client exists.

lifecycleScope.launch {
    voidhash.initialize()
}

It is safe to call initialize() repeatedly. Only the first successful call does work, and concurrent callers wait for it. A failed call leaves the client uninitialized so you can retry. Until initialize() runs, the client does no work on its own, so you can defer it until the customer consents or signs in.

Voidhash runs only in your app's main process. If you configure it in Application.onCreate, that code also runs in any other process of your app, such as a service declared with its own android:process. There configure returns an inert client that makes no requests and leaves the SDK's stored data alone. If your UI runs in a named process on purpose, set runInSecondaryProcess = true and configure Voidhash in that process only.

Calling configure again replaces the client. The previous client stops its background work and its Google Play connection. A purchase reported to it afterwards is handed to the new client when both use the same publishable key and base URL. Under a different key or base URL, the purchase is stored for the previous key and delivered once you configure a client with that key again. The previous client's identity, attribute and store methods throw CLIENT_SHUT_DOWN.

Until initialization has succeeded, every method that needs the schema throws VoidhashException with the code CONFIGURATION_MISSING.

Client options

Pass a VoidhashOptions value to configure to change the defaults.

val voidhash = Voidhash.configure(
    context = this,
    publishableKey = "vh_pk_...",
    options = VoidhashOptions(debug = BuildConfig.DEBUG),
)

Each option is described below.

OptionDefaultDescription
baseUrlhttps://api.voidhash.comOverrides the API origin for self-hosted deployments.
ingestUrlSame origin as baseUrlOverrides only the analytics origin.
debugfalseWrites SDK warnings to logcat and marks requests as debug-build traffic. Off, the SDK logs nothing.
distinctIdGenerated anonymous IDSeeds the initial customer identity and links an earlier anonymous identity to it. Usually omit it and call identify().
enabledtrueSet it to false to ship the SDK fully inert.
readOnlytrueForced on while commerce features are unavailable.
runInSecondaryProcessfalseRuns the SDK in a process other than the main one. Leave it off unless your UI runs in a named process.
onDiagnosticnullReceives reports about failures the SDK handled itself. See Diagnostics.

Every method on VoidhashClient is a suspending function, except capture, getDistinctId, and setReadOnly. The SDK does not read or write storage on the caller's thread, and it never posts work back to the main thread on its own. The one exception: called before initialize(), getDistinctId() and sessionId may wait briefly for the SDK's stored state to load.

Disabled clients

With enabled = false, every method is inert. The SDK makes no requests, opens no billing connection, runs no timers, and neither reads nor changes the data it stored while enabled, so that data is still there when you enable it again. Methods return empty values: getProducts() and getFeatureFlags() return an empty list, getCurrentPerson() returns null, and flush() and shutdown() report nothing sent. Reporting, restoring and capturing do nothing. purchase(...) throws CONFIGURATION_MISSING.

Observer mode

The initial release always uses observer mode. Passing readOnly = false or calling setReadOnly(false) cannot transfer Google Play ownership to Voidhash yet.

Products and transaction reporting

Load the product catalog and restore the customer's existing purchases.

val products = voidhash.getProducts()
voidhash.restorePurchases()

Initialization and restore submit observed Google Play transactions to Voidhash without acknowledging or consuming them. Only restorePurchases() asks Voidhash to move a purchase to the current user; background scans keep each purchase with the person it was recorded for. Google Play failures throw VoidhashException with a documented error code. purchase(...) remains in the SDK for a later commerce launch, but it currently raises READ_ONLY_PURCHASE_NOT_ALLOWED before it touches Play Billing.

After a successful host purchase, call reportTransaction(...) with its store transaction values. A valid report is persisted before it returns and delivered in the background, so a network or service outage delays reporting without failing the purchase flow. If the device cannot store the report, it throws TRANSACTION_NOT_DURABLE; delay consuming the purchase and report it again. Use syncPurchases() for callbacks without transaction values and for restore recovery. Store observers and scans cannot recover a consumable already finished or consumed by the host. See Report and restore purchases for callback examples, error handling, and scan limits.

People and entitlements

Read the current person, identify them when they sign in, and reset the client when they sign out.

voidhash.getDistinctId()
voidhash.identify(externalUserId = "user-123", email = "a@b.co", name = "Ada")
voidhash.setPersonAttributes(mapOf("plan" to "pro"))

val person = voidhash.getCurrentPerson(forceFetch = true)
val hasPremium = person?.hasActivePerk("premium") == true // the perk slug from Studio

voidhash.reset() // Sign out: clears the local identity and cache

The SDK caches the person snapshot for two days. After five minutes it treats the snapshot as stale but still serves it from the cache. Pass getCurrentPerson(forceFetch = true) to bypass the cache. The SDK refreshes the snapshot on its own after purchases, restores, and identity changes.

See Check access and Identify customers for the underlying concepts.

Feature flags

Evaluate the flags you need by key.

val flags = voidhash.getFeatureFlags(listOf("new_onboarding"))
val enabled = flags.firstOrNull { it.key == "new_onboarding" }?.enabled == true

Pass an empty list to evaluate every flag. See Evaluate feature flags for variants and identity behavior.

Analytics

Capture an event, and call flush() when you need the queue sent right away.

voidhash.capture("checkout_started", mapOf("source" to "paywall"))
voidhash.flush()

The SDK batches events, sending a request after 20 events or every 5 seconds while the app is in the foreground. Failed requests are retried with exponential backoff, so the wait grows after each failure, and the SDK honors the Retry-After header. flush() also sends queued attribute updates and purchase reports. Event names beginning with $ are reserved for Voidhash events. See Capture analytics.

Paywalls

Paywalls are flows drawn by the optional ui module. Pass VoidhashUiPaywallRenderer() as paywallRenderer, then call presentFlow(activity, location); it returns a FlowResult once the flow is gone. Without a renderer, presentFlow reports RENDERER_MISSING and the preloadPlacements option is inert. See Display a paywall.

Shutdown

Call shutdown() when the process is going away for good.

voidhash.shutdown()

It flushes analytics, ends the Play Billing connection, and stops the SDK's timers and callbacks. The client stops even if the flush fails or you cancel the call, for example with a timeout. Do not call it on every background event. The client stays shut down: Voidhash.shared returns null, and its identity, attribute and store methods throw CLIENT_SHUT_DOWN. A purchase reported to it is stored and delivered by the next client you configure with the same publishable key and base URL.