VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Guides
Launch
Go-live checklist
Make your first App Store purchase
Make your first Google Play purchase
Troubleshoot
Troubleshooting
Recover from incidents
SDK error codes
Purchase error codes
API error codes
FAQ
Recipes
Win back subscribers
Handle plan changes
Unlock the app after a web purchase
Link accounts across platforms
Handle refunds
Register devices for push
Migrate
Migrate to Voidhash
Migrate from RevenueCat
Migrate from Adapty
Migrate from Superwall
Run in observer mode
Privacy and hosting
Delete personal data
Self-hosting
Operate a self-hosted server

Run in observer mode

Run Voidhash next to another billing SDK and move purchase ownership when you are ready.

Observer mode lets another billing SDK keep control of purchases while Voidhash reads the same store transactions and builds each person's purchases and entitlements from them. Use it to adopt Voidhash in an app that already sells in-app purchases, then hand ownership over without a gap.

The one invariant

Both SDKs can run in the same build. Exactly one of them may finish a transaction (StoreKit) or acknowledge it (Google Play). The other may read it, report it and show UI based on it.

In observer mode the Voidhash SDK never finishes, acknowledges or consumes a transaction, and the Voidhash servers never acknowledge or consume a purchase they did not start. Your existing SDK stays the only owner. Every step below keeps this true, including the release where ownership moves.

Unacknowledged Google Play purchases are refunded

Google Play refunds a purchase that nobody acknowledges within three days. An ownership gap shows up as lost revenue, not as an error.

Turn on observer mode

Set readOnly when you configure the SDK:

AppDelegate.swift
import Voidhash

var options = VoidhashOptions()
options.readOnly = true

let voidhash = Voidhash.configure(publishableKey: "vh_pk_...", options: options)
App.kt
import com.voidhash.sdk.Voidhash
import com.voidhash.sdk.VoidhashOptions

val voidhash = Voidhash.configure(
    context = this,
    publishableKey = "vh_pk_...",
    options = VoidhashOptions(readOnly = true),
)
src/lib/voidhash.ts
import { createVoidhashClient } from "@voidhash/react-native";

export const voidhash = createVoidhashClient("vh_pk_...", { readOnly: true });
BehaviorObserver modeOwner mode (default)
Starts purchasesNoYes
Observes store transactionsYesYes
Sends transactions to VoidhashYesYes
Finishes or acknowledges transactionsNoYes, after Voidhash accepts it
Reads products, people, perks, flagsYesYes

On launch and each time the app returns to the foreground, the SDK sends the purchases the store currently reports. In observer mode it also reports transactions the store delivers later, such as renewals and Ask to Buy approvals.

Blocked in observer mode

Starting a purchase fails with READ_ONLY_PURCHASE_NOT_ALLOWED before the store is touched. A purchase button on a Voidhash paywall fails the same way. Restores, products, people, perks, feature flags, paywall lookups, analytics and attribute updates keep working.

Report purchases your billing SDK makes

Call reportTransaction after each successful purchase your existing SDK makes, and for each transaction its restore returns. Voidhash needs only the Apple transaction ID or the Google Play purchase token; it verifies the rest with the store. Reporting never finishes or acknowledges the transaction.

try await voidhash.reportTransaction(transactionId: transactionId)
voidhash.reportTransaction(purchaseToken)
await voidhash.client.reportTransaction({ platform: "ios", transactionId });
await voidhash.client.reportTransaction({ platform: "android", purchaseToken });

Pass isRestore: true for a transaction from the user's own restore. When a callback exposes no transaction, call syncPurchases() to send what the store currently reports. Each SDK's purchases page covers the accepted inputs and errors: Swift, Kotlin, React Native.

Switch ownership at runtime

Flip the mode on the running client instead of configuring a new one, which would drop the store connection, caches and analytics queue.

voidhash.setReadOnly(false) // Voidhash owns purchases
voidhash.setReadOnly(true)  // back to observing
voidhash.setReadOnly(false) // Voidhash owns purchases
voidhash.setReadOnly(true)  // back to observing
voidhash.client.setReadOnly(false); // Voidhash owns purchases
voidhash.client.setReadOnly(true); // back to observing

isReadOnly returns the current mode. A purchase already in progress finishes under the mode it started with, so switching mid-purchase never leaves a transaction unfinished.

Decide both halves of ownership from one flag: which SDK's purchase UI appears, and which SDK finishes transactions. Set the mode before you show purchase UI, not after a tap:

app/upgrade-entry.tsx
import { useEffect } from "react";

import { voidhash } from "../src/lib/voidhash";

export default function UpgradeEntry() {
  const voidhashOwnsPurchases = useMyFeatureFlag("voidhash_purchases");

  useEffect(() => {
    voidhash.client.setReadOnly(!voidhashOwnsPurchases);
  }, [voidhashOwnsPurchases]);

  return voidhashOwnsPurchases ? <VoidhashUpgradeScreen /> : <LegacyUpgradeScreen />;
}

When Voidhash has nothing to show for a location, presentFlow returns a failed result without moving money, so showing your existing paywall instead is always safe. See Paywalls in your app.

To ship the SDK completely inert, with no store connection or network, configure it with enabled: false. Unlike readOnly, that is fixed when the client is created.

Migration sequence

Ship Voidhash in observer mode

Add the SDK with readOnly on and report your existing SDK's transactions. The purchase flow does not change in this release.

Verify in the sandbox

On each platform, make a purchase, then renew, cancel, expire, refund and restore it. Each should appear on the person in People with the perks you expect. See Test purchases (Help center).

Compare with your current source of truth

For a sample of real subscribers, compare their perks in Voidhash with the access your current system grants. Resolve every mismatch before you continue.

Move purchase UI and ownership together

Behind one flag, as shown above, let Voidhash present the paywall and own purchases. Roll it out to a small share of users, then widen it. Never split UI and ownership across two releases.

Remove the previous SDK

Once the flag is at 100% and transactions reconcile, delete the old integration and the readOnly option.

Retest purchases, renewals, cancellations and restores on both platforms after each step.

Next steps

  • Migrate to Voidhash
  • Test purchases (Help center)
  • Check access from your backend with Node.js, PHP, Go, Rust or over HTTP

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

PreviousMigrate from SuperwallNextDelete personal data

On this page

The one invariantTurn on observer modeBlocked in observer modeReport purchases your billing SDK makesSwitch ownership at runtimeMigration sequenceShip Voidhash in observer modeVerify in the sandboxCompare with your current source of truthMove purchase UI and ownership togetherRemove the previous SDKNext steps