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.
minSdk23.compileSdk35. 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.
| Option | Default | Description |
|---|---|---|
baseUrl | https://api.voidhash.com | Overrides the API origin for self-hosted deployments. |
ingestUrl | Same origin as baseUrl | Overrides only the analytics origin. |
debug | false | Writes SDK warnings to logcat and marks requests as debug-build traffic. Off, the SDK logs nothing. |
distinctId | Generated anonymous ID | Seeds the initial customer identity and links an earlier anonymous identity to it. Usually omit it and call identify(). |
enabled | true | Set it to false to ship the SDK fully inert. |
readOnly | true | Forced on while commerce features are unavailable. |
runInSecondaryProcess | false | Runs the SDK in a process other than the main one. Leave it off unless your UI runs in a named process. |
onDiagnostic | null | Receives 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 cacheThe 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 == truePass 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.