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() returns an Err with 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.

// Apple: keep the transaction ID as a string, preserving every digit.
await voidhash.client.reportTransaction({ platform: "ios", transactionId });

// Google Play: the purchase token, not the order ID.
await voidhash.client.reportTransaction({ platform: "android", purchaseToken });

You can report as soon as the client exists. A report made while the client is still initializing, or after end(), waits for initialization and is then stored. If the host exposes original store data instead, the same method accepts a StoreKit JWS string or a Play bridge object with originalJson. Voidhash extracts the identifier internally and discards the remaining payload; no receipt-decoding dependency is needed in your application.

Legacy ReportedTransaction metadata such as productId, purchaseDate, quantity, receipt and appAccountToken is optional and discarded. Android's optional purchaseState can mark a report as pending or unspecified; those reports are ignored until reported as purchased. The original Play JSON's pending state is converted internally. Multi-product bridge purchases are rejected because the current purchase processing supports one product per purchase.

reportTransaction() returns Result<void, VoidhashError>. Missing or malformed required identifiers return FAILED_TO_REPORT_TRANSACTION; VOIDHASH_CLIENT_NOT_INITIALIZED means the client could not initialize. Network and delivery failures leave the report queued and return Ok.

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 captured in the durable outbox before returning; delivery runs in the background. Success does not imply immediate backend acceptance. SDK diagnostics describe deferred delivery. Duplicate reports and observer callbacks use the same store identifier and retain the original captured identity through retries and app relaunches.

The SDK remembers on the device which store transaction Voidhash last accepted for each purchase, and who owns the purchase. A receipt Voidhash already has is never sent again by a report, an observer callback or a scan, even after the user signs out or another user signs in on the same device. A subscription renewal and an App Store refund or revocation are still reported, under the purchase's owner.

While the app is open, a report that could not be delivered is retried on its own timer, and again at launch, on foreground, when connectivity returns and on flush(). Queued identity links are sent first, so a purchase made right after identify() lands on the identified user. A receipt Voidhash refuses with a verdict that cannot change on its own, for example because the App Store or Google Play is not configured for this app, stays queued and is reported once through onDiagnostic as TRANSACTION_REJECTED. It is sent again at the next launch or after six hours.

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 sends receipts already queued on the device, reports purchases the store still exposes, skips receipts already accepted by Voidhash, and refreshes the current person. It never moves a purchase to another user. It does not prompt for store authentication. Store-read failures reach the caller; delivery failures remain queued.

const result = await voidhash.client.syncPurchases();
if (result.isErr()) console.warn("Voidhash purchase scan failed", result.error);

Initialization installs the observer and scans purchases in the background. A foreground scans again at most once a minute; a connectivity restore can scan again ten seconds after the previous recovery. 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, including subscriptions whose auto-renew is off. On an iOS device where in-app purchases are restricted by Screen Time or device management, purchases are still observed, scanned and restored.

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.

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 purchase retains its original buyer and must be delivered before the restore can request another owner. A restore still queued when the app closes keeps that request after a relaunch.

const result = await voidhash.client.restorePurchases();
if (result.isErr()) console.warn("Voidhash restore failed", result.error);

On iOS, this calls AppStore.sync() and reads verified non-consumable transaction history, including expired or revoked transactions. App Store authentication may appear, so call it only from a user action. Android queries currently owned Play purchases; it cannot retrieve consumed purchases or arbitrary expired subscription history. Known consumables are excluded from explicit restoration. The restore waits for the current transaction of each purchase; older transactions of a subscription are revalidated in the background, so a long subscription history does not hold the Restore button. A store error, or a receipt Voidhash could not confirm yet, fails the restore and the receipt stays queued for retry. A receipt Voidhash refuses outright does not fail it and is reported as TRANSACTION_REJECTED. 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.

Load products

useProducts() returns the store-backed products configured in your project. Prices come already formatted for the customer's storefront, so you can render them directly.

const { data: products, error, isLoading } = voidhash.useProducts();
const monthly = products.get("monthly");

data.get(slug) returns the product for one of the project's product slugs, and data.toList() returns every product the store returned. data.bySlug maps every configured slug to its product, or null. monthly is undefined when the store does not return that product. That usually means the product is not available in the current sandbox account, country, app version, or provider configuration. The hook loads again when your project's product configuration changes, so a screen opened on a first launch fills in once the configuration arrives.

Once you have a product, render its store-provided displayPrice.

if (!monthly) return null;

return <Text>{monthly.displayPrice}</Text>;

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_REJECTED means Voidhash refused the receipt, usually because the App Store or Google Play is not configured for this app's bundle ID or package name, or the purchase belongs to another store environment. Analytics events arriving does not prove that a store transaction was discovered or submitted.
  • 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