Errors

Structured error codes and recovery guidance for the Kotlin SDK.

When an SDK call fails, it throws a VoidhashException. That includes Google Play Billing failures: the SDK converts them, so a store problem never reaches your app as another exception type, and code that catches VoidhashException (or any Exception) handles it. Every exception carries a stable, machine-matchable code string. Match on error.code when the recovery differs by cause, and report the full error for everything else.

This example handles one specific code and lets every other error propagate:

try {
    val products = voidhash.getProducts()
} catch (error: VoidhashException) {
    if (error.code == "FAILED_TO_GET_PRODUCTS") {
        // retry, or render the screen without store metadata
    }
}

Most calls never throw because Voidhash is unreachable. Reads such as getCurrentPerson() and getFeatureFlags() answer from cached state, flush() reports what is still queued, and writes queue for later delivery. The codes below are the failures that do reach your code. The message text is informational. Never match on it.

Store codes

CodeThrown byRecovery guidance
FAILED_TO_GET_PRODUCTSgetProducts()Google Play did not return product details, for example while the device is offline. Retry, or render the screen without store metadata.
BILLING_CONNECTION_FAILEDsyncPurchases(), restorePurchases()The Play Billing connection did not open: Google Play services are missing or updating, Play Billing is unavailable for the signed-in account, or setup did not finish in time. A later call retries it.
STORE_UNAVAILABLEsyncPurchases(), restorePurchases()Google Play could not list the purchases it owns. Retry later.
RECONCILE_TRANSACTIONS_FAILEDrestorePurchases(), syncPurchases()See below.

restorePurchases() throws RECONCILE_TRANSACTIONS_FAILED when at least one restorable receipt was not accepted. That includes a receipt still queued because Voidhash could not be reached; the SDK keeps it and delivers it in the background. It also includes a receipt the backend refused, for example because Google Play is not configured for your app's package name. Prompt the customer to try the restore again later. syncPurchases() throws it only when a receipt could not be processed at all.

Reporting codes

CodeThrown byRecovery guidance
INVALID_TRANSACTIONreportTransaction(...)Supply a non-empty Google Play purchase token. Multi-product purchases are unsupported.
TRANSACTION_NOT_DURABLEreportTransaction(...)The device could not store the receipt, for example because its disk is full. Delivery is still attempted, but the receipt would not survive the app closing. Delay consuming the purchase and report it again later.

Purchase codes

CodeThrown byRecovery guidance
READ_ONLY_PURCHASE_NOT_ALLOWEDpurchase(...)Purchase initiation is unavailable in the observer-only release.
CONFIGURATION_MISSINGpurchase(...)The SDK is disabled, or no project schema has loaded yet. Retry once one has.

USER_CANCELLED, PURCHASE_PENDING, PURCHASE_FAILED and BILLING_ERROR are reserved for SDK-started purchases in the later commerce launch.

Argument codes

CodeThrown byRecovery guidance
INVALID_ARGUMENTidentify(...), setPersonAttributes(...)A distinct ID was empty or only whitespace, longer than 255 characters, contained a NUL character or started with vh:anon:, or an attribute value was a map or a list. Fix the call; nothing was sent or queued.

Lifecycle codes

CodeThrown byRecovery guidance
CLIENT_SHUT_DOWNidentify(...), setPersonAttributes(...), reset(), getProducts(), purchase(...), restorePurchases(), syncPurchases()The client was shut down, or replaced by a later configure(...). Use the client configure(...) returned last, or Voidhash.shared.

API codes

A write the API refuses outright throws a VoidhashApiException. It is a VoidhashException whose status and tag describe the API's answer. Branch on code and tag rather than on the status. identify(...) throws one in these cases and restores the previous identity:

StatusTagMeaning
400Api/SdkValidationErrorThe request is invalid.
404Api/SdkPersonNotFoundErrorVoidhash does not know the target.

A 409 (Api/SdkPersonAlreadyIdentifiedError) means the anonymous identity is already linked to another account. identify(...) does not throw for it: the device switches to the customer you identified without merging the anonymous identity, and the SDK reports PERSON_WRITE_REJECTED.

setPersonAttributes(...) throws for a payload the API refuses, such as a 400. Its "no such person" answer is not a refusal: the attributes stay queued until the person exists.

Writes that fail because Voidhash is unreachable, because an answer could not be read, or because the publishable key was rejected are queued instead and report WriteStatus.DEFERRED. INVALID_REQUEST is thrown before anything is sent, when the SDK cannot build the request at all.

CodeMeaning
API_ERRORThe API refused the request. Check status and tag; sending it again gets the same answer.
AUTHENTICATION_FAILEDThe publishable key was rejected. Outbound traffic pauses, queued data is kept, and writes report DEFERRED instead.
INVALID_REQUESTNo request could be built: check the base URL and publishable key.

Diagnostics

Failures the SDK handles on its own never throw. They are reported through the onDiagnostic option instead, as a VoidhashDiagnostic with a kind, a stable code, the operation, and whether the SDK retries. The handler runs on a background thread, one report at a time in the order they were made, and an exception it throws is ignored. Messages never contain a user ID or a purchase token.

VoidhashOptions(
    onDiagnostic = { diagnostic ->
        Log.d("Voidhash", "${diagnostic.code}: ${diagnostic.message}")
    },
)
CodeMeaning
AUTHENTICATION_FAILEDThe publishable key was rejected. Outbound traffic pauses and queued data is kept. Reported once per pause; AUTHENTICATION_RECOVERED follows.
CIRCUIT_OPENVoidhash failed five times in a row, so requests are skipped and cached data is served until a retry succeeds. CIRCUIT_CLOSED reports the recovery.
TRANSACTION_SYNC_DEFERREDA receipt is still queued after a delivery attempt that failed or was not confirmed. It is retried.
TRANSACTION_REJECTEDVoidhash refused a receipt, for example because Google Play is not configured for the package. It is sent again on the next launch.
PERSON_WRITE_REJECTEDA queued identify or attribute update was dropped because the API refused it, or a sign-in could not merge an anonymous identity another account owns.
ANALYTICS_SEND_FAILEDAnalytics events were not delivered and are retried.
ANALYTICS_EVENT_DROPPEDAn event was dropped at the 1,000-event queue limit, or refused by the API. Only the refused event is dropped, never the rest of its batch.
ANALYTICS_EVENT_INVALIDAn event was not captured because its name was blank, or because there was no distinct ID to attribute it to.
SCREEN_MAPPER_FAILEDYour mapScreen function threw, so that screen was not captured. The exception does not reach your app.
CACHE_READ_FAILEDAn unreadable cache entry was discarded.
CACHE_WRITE_FAILEDThe device could not store a value. It is kept in memory and stored with the next write that succeeds.
TRANSACTION_OUTBOX_BACKLOGMore than 1,000 receipts are waiting for delivery. None was dropped.

The package README lists every code.

Next steps