VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Groups
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Kotlin SDK
Kotlin quickstart
Display a paywall
Make and restore purchases
Check access
Identify users
Evaluate feature flags
Capture analytics
Groups
Track screens
Development mode
Kotlin SDK reference
Errors
Diagnostic codes
Troubleshooting

Errors

Exception types, error codes and diagnostics of the Kotlin SDK, with what to do for each.

The Kotlin SDK throws a VoidhashException with a stable code: Google Play Billing failures are mapped to the SDK's own codes, and Voidhash API failures are mapped from the HTTP status.

Handle errors

Suspending methods throw on failure. Rethrow CancellationException so coroutine cancellation keeps working, then branch on code. The message is for people; never match on it.

import com.voidhash.sdk.VoidhashException
import kotlinx.coroutines.CancellationException

lifecycleScope.launch {
    try {
        voidhash.purchase(activity, monthly)
    } catch (error: CancellationException) {
        throw error
    } catch (error: VoidhashException) {
        when (error.code) {
            "USER_CANCELLED", "PURCHASE_PENDING" -> Unit // Expected outcomes, not failures.
            "OFFER_UNAVAILABLE" -> showRegularPrice()
            else -> showPurchaseError(error)
        }
    }
}

Most calls don't throw when Voidhash is unreachable: reads answer from the cache, writes are queued, and flush() reports what is still pending. Failures the SDK handles itself are diagnostics.

Error types

TypeCodeThrown for
VoidhashExceptionAnyBase type of every SDK error. Has code and description.
VoidhashApiExceptionAPI_ERROR, AUTHENTICATION_FAILED, RATE_LIMIT_EXCEEDED, MALFORMED_RESPONSEVoidhash answered with an error. Adds status, tag and retryAfterMs.
VoidhashNetworkExceptionNETWORK_ERRORA request never reached Voidhash. Usually handled by the SDK; it can appear as another error's cause.
FlowExceptionFLOW_<reason>Carried by FlowResult.Failed, never thrown. Adds reason and abiVersion.

Error codes

Configuration

CodeWhenWhat to do
CONFIGURATION_MISSINGThe client is disabled, or the project configuration hasn't loaded yet.Retry after initialize().
CLIENT_SHUT_DOWNThe client was shut down or replaced by a later configure.Use Voidhash.shared.
READ_ONLY_PURCHASE_NOT_ALLOWEDpurchase ran in observer mode.Buy through your other billing SDK.
PRODUCT_NOT_FOUNDIn development mode, the product has no development data.Give the product a billing duration.
DEVICE_LOCKEDA call ran before the device was first unlocked after a restart, and couldn't be held until then. See Create the client.Call again once the device is unlocked.

AUTHENTICATION_FAILED, a rejected publishable key, is listed under Network and API.

Products and purchases

CodeWhenWhat to do
USER_CANCELLEDThe person closed the Google Play sheet.Nothing.
PURCHASE_PENDINGThe payment awaits approval.Nothing. When Google Play completes it, the SDK sends and acknowledges it, even after a relaunch.
OFFER_UNAVAILABLEGoogle Play doesn't offer this offerId to the person, or the mapping has no such offer. Nothing was bought.Offer the regular price.
PURCHASE_FAILEDGoogle Play returned no purchase, or the subscription has no base plan offer.Check the product's Google Play mapping.
PURCHASE_UNKNOWN_RESULTGoogle Play returned a purchase in an unknown state.Report the error.
PURCHASE_IN_PROGRESSAnother Google Play purchase sheet is already open.Wait for it to close.
PURCHASE_EMPTYGoogle Play reported success but returned no purchase.Report the error; refresh the person before retrying.
REPLACED_SUBSCRIPTION_NOT_OWNEDpurchase with replacing named a subscription the Google Play account doesn't own. Nothing was bought.Buy without replacing, or name the subscription the person has. See Change a subscription.
SUBSCRIPTION_REPLACEMENT_REFUSEDGoogle Play refused the replacement before its sheet opened. Nothing was bought.Report the error; the message carries Google Play's reason.
SKU_NOT_FOUNDGoogle Play doesn't sell the product, or its details weren't loaded.Check the product ID in the Play Console and the mapping.
ITEM_ALREADY_OWNEDThe person already owns the product, so Google Play won't sell it again.Offer to restore. The SDK delivers and finishes what it owns of the product, so a consumable can be bought again.
BILLING_ERRORGoogle Play reported any other failure while buying.Report the error.
GOOGLE_BILLING_NOT_INITIALIZEDGoogle Play services are missing, or Play Billing setup didn't finish.Let the person retry later.
CURRENT_ACTIVITY_NULLNo activity was available to launch the sheet from.Pass a visible activity.
CONNECTION_ENDEDThe billing connection closed during the purchase, for example on shutdown.Let the person retry.
FAILED_TO_GET_PRODUCTSgetProducts() got no product details, for example offline.Retry, or render without prices.
BILLING_CONNECTION_FAILEDrestorePurchases() or syncPurchases() couldn't connect to Play Billing.A later call retries.
STORE_UNAVAILABLEpurchase couldn't reach Google Play, or restorePurchases() or syncPurchases() couldn't list owned purchases.Retry later. If the person completed the purchase anyway, the SDK finds it and finishes it.
RECONCILE_TRANSACTIONS_FAILEDA restore or sync couldn't deliver a purchase to Voidhash.Ask the person to retry later. The SDK keeps the purchase and retries.
INVALID_TRANSACTIONreportTransaction got a blank purchase token or a multi-product purchase.Pass a single-product purchase.
TRANSACTION_NOT_DURABLEThe device couldn't store a reported purchase, for example with a full disk or before its first unlock.Don't consume the purchase yet; report it again.

syncPurchases() throws RECONCILE_TRANSACTIONS_FAILED only when a purchase couldn't be processed at all; restorePurchases() also throws it while a purchase is still queued, or when Voidhash hasn't answered within 10 seconds. The restore then continues in the background.

When Google Play reported the failure, the exception's cause is a BillingEngineException whose responseCode is Google Play's own response code.

Paywalls and flows

presentFlow never throws. It returns FlowResult.Failed, whose error.reason names the cause.

CodeWhenWhat to do
FLOW_NOT_ASSIGNEDNothing is assigned to the location, it doesn't exist or is archived, or the build needs a newer SDK.Show your fallback screen.
FLOW_UNAVAILABLENothing is cached and Voidhash is unreachable, the paywall couldn't be downloaded, or it wasn't ready to show within 15 seconds.Show your fallback; retry later.
FLOW_RENDERER_MISSINGpaywallRenderer isn't set.Add the renderer.
FLOW_UNSUPPORTED_PACKAGEThe paywall was built in a format this SDK can't read.Update the SDK.
FLOW_PRESENTATION_FAILEDThe paywall couldn't be shown, or three frames in a row failed to draw while it was on screen.Present from a visible activity; report the error.
FLOW_DISABLEDThe client is disabled or shut down.Nothing.

See Handle failures.

Identity

CodeWhenWhat to do
INVALID_ARGUMENTidentify got a user ID that is blank, longer than 255 characters, contains NUL, or starts with vh_anon_ or vh:anon:. Or setPersonAttributes got a map or list value. Nothing was sent.Fix the call. See Identify users.

identify throws a VoidhashApiException with code API_ERROR when Voidhash refuses the sign-in, and restores the previous identity. Its tag is Api/SdkValidationError (400) or Api/SdkPersonNotFoundError (404). A 409 doesn't throw. Writes that fail because Voidhash is unreachable, unreadable, rejects the key, or takes longer than 2 seconds are queued and return WriteStatus.DEFERRED.

Network and API

CodeWhenWhat to do
AUTHENTICATION_FAILEDVoidhash itself answered 401 or 403: the publishable key was rejected. getCurrentPerson and getFeatureFlags throw it once, when nothing is cached.Check the key and its project. See API keys.
RATE_LIMIT_EXCEEDEDVoidhash answered 429.Wait retryAfterMs; don't retry in a loop.
API_ERRORAny other error status.Branch on tag, not on status; report the rest.
MALFORMED_RESPONSEVoidhash's answer couldn't be read.Report the error.
NETWORK_ERRORThe request never reached Voidhash.Retry later.
INVALID_REQUESTNo request could be built.Check baseUrl and the publishable key.

A 401 or 403 from something in front of Voidhash, such as a proxy, a firewall or an edge page, isn't a rejected key. The SDK treats it as a temporary failure: it retries, and nothing pauses.

Diagnostics

Failures the SDK handles itself never throw. Receive them with the onDiagnostic option as a VoidhashDiagnostic with a kind, code, operation, message, and whether the SDK retries. The handler runs on one background thread, in order; exceptions it throws are ignored. Messages never contain a user ID or purchase token.

VoidhashOptions(
    onDiagnostic = { diagnostic ->
        Log.d("Voidhash", "${diagnostic.code}: ${diagnostic.message}")
    },
)

Diagnostic codes lists every code and what it means.

Next steps

  • Troubleshooting by symptom.
  • Make and restore purchases.
  • Error codes across platforms.

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousKotlin SDK referenceNextDiagnostic codes

On this page

Handle errorsError typesError codesConfigurationProducts and purchasesPaywalls and flowsIdentityNetwork and APIDiagnosticsNext steps