Report and restore purchases

Report purchases from your existing billing integration to Voidhash.

The initial release runs in observer mode. Your existing billing integration starts purchases, restores access, and finishes or acknowledges store transactions. Voidhash reports the purchases it discovers without taking over those responsibilities. SDK-started purchases and hosted paywalls are temporarily unavailable; purchase(...) throws READ_ONLY_PURCHASE_NOT_ALLOWED before opening a store purchase flow.

Report a completed purchase or restore

Call reportTransaction(...) after each successful purchase, including purchases from another SDK's paywall. Report each transaction returned by a restore callback as well. Configure Voidhash and identify the purchasing user before starting the host purchase flow.

Only the store identifier is required: an Apple transaction ID or a Google Play purchase token. Voidhash supplies its configured bundle/package and captures the current SDK identity automatically. The backend fetches and verifies the purchase with the store; timestamps, quantities, product metadata, receipts, signatures and purchaser arguments are unnecessary.

// In an app-owned coroutine; use the purchase token, not the order ID.
voidhash.reportTransaction(purchaseToken)

You can also pass a Google Play Billing Purchase directly. Voidhash reads its token and purchase state, then discards other fields, original JSON and signature. Pending purchases are ignored; report them again when purchased. A token-only report from a completed callback needs no purchase time, quantity, product ID or order ID. The server verifies the current state with Google Play.

Blank tokens and multi-product Purchase objects throw INVALID_TRANSACTION. Reporting does not require a Billing connection; delivery failures stay queued without throwing into the host purchase callback.

Reporting works after the host has finished or consumed a purchase because it does not scan the store. It never finishes, acknowledges or consumes transactions. Keep your billing integration's finalization and access checks in place.

A successful report is written to the device before returning; delivery runs in the background. Success does not imply immediate backend acceptance. Each delivery attempt that fails or is not confirmed is reported through onDiagnostic as TRANSACTION_SYNC_DEFERRED; the receipt stays queued and is retried. If the device cannot store the report, for example because its disk is full, reportTransaction throws TRANSACTION_NOT_DURABLE. Delivery is still attempted, but the report would not survive the app closing, so delay consuming the purchase and report it again later. Duplicate reports and observer callbacks use the same store identifier and retain the original captured identity through retries and app relaunches. A purchase token Voidhash has already accepted is not sent again.

Recovery scans and restore callbacks

Use syncPurchases() for silent recovery when the host callback exposes no usable store transaction values, including after its restore flow. It reports purchases the store still exposes, skips receipts already accepted by Voidhash, and refreshes the current person. It does not prompt for store authentication. Store-read failures reach the caller as BILLING_CONNECTION_FAILED or STORE_UNAVAILABLE; delivery failures remain queued.

Silent syncs, background scans and reports never move a purchase between people. The device remembers which receipts Voidhash accepted and for whom, so when another user signs in on the same device and Google account, scans do not submit the earlier user's purchases for them. A renewal is reported for the person who owns the subscription. Only restorePurchases() asks Voidhash to apply your project's transfer policy.

// In an app-owned coroutine; handle errors as above.
voidhash.syncPurchases()

Initialization installs the observer and scans purchases in the background. Foreground and connectivity recovery also scan with a one-minute throttle per trigger. In observer mode, scans can report unconsumed or unfinished consumables that remain visible without finalizing them. They cannot recover consumed or finished consumables, or arbitrary expired subscription history. iOS scans pending transactions and current entitlements; Android queries currently owned purchases.

syncPurchases() is a fallback, not a delivery guarantee. If the host hides transaction values and finishes a consumable before Voidhash captures it, a later callback or scan cannot recover it. Keep explicit reporting in the successful purchase callback whenever your host SDK exposes the values.

Load products

getProducts() returns the store-backed products configured in your project. displayPrice comes already formatted for the customer's storefront, so you can render it directly. price is the same amount as a number (59.99 for "59,99 €") and currency is its ISO code. For a subscription, both describe the recurring price of the base plan configured in your project, not a free trial or introductory phase ahead of it; billingPeriod is that price's period and googlePlayOfferToken is the base plan's own offer. A Google Play failure throws FAILED_TO_GET_PRODUCTS.

val products = voidhash.getProducts()
val monthly = products.firstOrNull { it.slug == "monthly" }

If a product is missing from the result, the store did not return it. That happens when the product is not available on the current store account, environment, or provider configuration.

Explicit restoration

Connect your Restore Purchases button to restorePurchases(). It revalidates restorable purchases for the identity that requested the restore, even when Voidhash already accepted those receipts. It is the only call that asks Voidhash to apply the project's transfer policy, which controls whether ownership changes. A queued restore keeps that request across app relaunches. A queued purchase retains its original buyer and must be delivered before the restore can request another owner.

voidhash.restorePurchases()

This queries currently owned Play purchases. Google Play has no separate restore prompt and does not return consumed purchases or arbitrary expired subscription history. Known consumables are excluded from explicit restoration. A store error fails the restore with BILLING_CONNECTION_FAILED or STORE_UNAVAILABLE. A receipt that was not accepted fails it with RECONCILE_TRANSACTIONS_FAILED: a deferred receipt stays queued for retry, and one the backend refused stays queued and is retried on the next launch. An empty store can restore successfully. Success does not imply an active entitlement: use the person's current grants to decide whether to unlock access.

Reporting and silent sync after a host restore discover purchases. Use explicit restoration when Voidhash must revalidate cached receipts or apply its ownership policy for the current user.

Transactions missing from the dashboard

  • Confirm successful purchase callbacks call reportTransaction(...) with the original store values. Use syncPurchases() for restore-only callbacks that expose no transaction values.
  • Verify that Voidhash uses the intended project key and purchasing user, and that the dashboard is showing the matching project, person, and store environment.
  • Check SDK diagnostics for store-read or delivery failures: TRANSACTION_SYNC_DEFERRED means a receipt is still waiting for delivery. Analytics events arriving does not prove that a store transaction was discovered or submitted.
  • A TRANSACTION_REJECTED diagnostic means the backend refused the receipt and will keep doing so, for example because Google Play is not configured for the app's package name (such as a debug build's applicationId suffix) or the token belongs to another app. The receipt stays on the device and is sent again on the next launch, or after six hours in a long session; fix the configuration and relaunch.
  • For scan-only integrations, confirm the purchase is still exposed by the store. A finished or consumed consumable requires captured transaction values; a successful host purchase alone is insufficient for a later scan.

Next steps