Mobile purchase claims
Connect mobile purchases to FounderHQ contacts.
Tie an App Store or Google Play purchase to the person who made it, and to the account it should fund. Allow about fifteen minutes.
Before you start
Install the FounderHQ SDK (React Native, iOS, or Android) and connect a store: RevenueCat, Superwall, Apple, or Google Play.
A store purchase carries no browser session and no metadata field you can fill. It carries one UUID. The SDK mints that UUID, records what it stands for, and hands it to the store. When the store notifies FounderHQ, the UUID leads back to the buyer.
Prepare before you open the purchase sheet
const prepared = await founderhq.preparePurchase({ source: "app_store" });prepared gives you the field your store library needs:
| Store | Field to pass | Where it goes |
|---|---|---|
| App Store | prepared.appAccountToken | StoreKit purchase option appAccountToken |
| Google Play | prepared.obfuscatedExternalAccountId | BillingFlowParams.Builder.setObfuscatedAccountId |
| Superwall | prepared.appAccountToken | Superwall user attribute fhq_visitor, plus the store field above |
preparePurchase waits a moment for FounderHQ to acknowledge the context, then
returns anyway. Checkout never blocks on the network.
Whatever account is set on the SDK at this moment is frozen into the context. Changing accounts later does not move this purchase.
Report the purchase result
await founderhq.observePurchase({
prepared,
purchase: {
source: "app_store",
transactionId: transaction.id,
originalTransactionId: transaction.originalID,
},
});For Google Play, send { source: "google_play", purchaseToken, orderId }.
The source you observe must match the source you prepared.
Using RevenueCat? Wrap the purchase instead, and the SDK does both steps:
const result = await founderhq.purchaseWithRevenueCat(() =>
Purchases.purchasePackage(pkg),
);Move an existing subscription to an account
Sometimes a person already pays, and later that subscription should fund a team, family, or workspace. That is a deliberate product action, so it needs a deliberate call:
await founderhq.claimSubscription({
purchase: {
source: "app_store",
transactionId: transaction.id,
originalTransactionId: transaction.originalID,
},
confirmation: "move_future_revenue",
});A claim moves future revenue only
Payments that already happened keep the owner they had. The new owner starts at the moment FounderHQ accepts the claim. MRR moves; history does not. Refunds always follow the original payment.
Never call this from a listener, a restore, an entitlement refresh, or a sign-in. Call it from a screen where the person asked for it.
A purchase the app did not prepare
With Revenue owner set to Organization, a store purchase without a prepared context has no account of its own. FounderHQ uses the buyer's accounts, as for a web payment:
- A buyer in exactly one account pays for that account.
- A buyer in several accounts, or in none, pays as a contact.
- A buyer that FounderHQ does not know leaves the purchase unmatched.
Renewals keep the owner of the first payment. A purchase claim for the same purchase that arrives later moves the payment, the subscription, and its MRR to the claim's account, also when FounderHQ had chosen the buyer's one account. The reverse never happens: a purchase that a prepared context or a claim names keeps its account, whatever the buyer's accounts are later. To name the account from the start, prepare the purchase (step 1) while the account is set on the SDK. See Which account gets a payment.
The RevenueCat identity rule
If you use RevenueCat, the FounderHQ UUID is the RevenueCat App User ID. Let
the SDK configure and log in RevenueCat, and never call
Purchases.logOut(). See Connect RevenueCat.
The same calls on every platform
| What you do | React Native | iOS | Android |
|---|---|---|---|
| Prepare | preparePurchase | preparePurchase(source:) | preparePurchase(source, completion) |
| Apply the token | pass prepared fields | purchase(_:options:) adds it | applyPurchaseContext(builder, prepared) |
| Report the result | observePurchase | observePurchase(_:prepared:) | observePurchase(purchase, prepared) |
| Wrap RevenueCat | purchaseWithRevenueCat | purchaseWithRevenueCat | revenueCatPurchaseCallback |
| Move a subscription | claimSubscription | claimSubscription(_:confirmation:) | claimSubscription(purchase, confirmation) |
From your backend
Your server can do two things with a secret key, through the Node SDK:
accountContextToken({ account, userId, idempotencyKey })issues short-lived proof that this person may act for this account. It moves no money.claimMobileSubscription({ account, userId, purchase, confirmation, idempotencyKey })moves a subscription's future revenue to an account, with the same"move_future_revenue"confirmation and the same rules as the mobile call.
Neither call confirms the account or the membership. Declare those with
upsertAccount and accountMembership. The token is issued at once, but a
claim uses it only when the account is confirmed and the person is a declared
member when the claim is processed. A claim that arrives before that counts as
a claim without a token.
Verify
- Make a real purchase in the app. FounderHQ tracks live purchases only.
- In FounderHQ, open Analytics → People and open the buyer. The purchase is on their Revenue tab.
- After a transfer claim, the next renewal counts for the account, and the earlier payments still count for the person.
Troubleshoot
The purchase has no contact
The app purchased without preparing. Call preparePurchase first, and pass the
token to the store library.
"Observed purchase source does not match its preparation"
You prepared for one store and observed another. Prepare with the same source you buy with.
A RevenueCat purchase reports no transaction
Some observer and custom-completion modes return no store transaction. The SDK will not guess from entitlements. Claim it later with a verified transaction ID or purchase token.