Claiming the Result
How what a visitor did on a web funnel reaches your app — the claim token, the four ways an install finds it, and exactly which fields you get back.
A web funnel runs in a browser and your app runs on a device. Claiming is how the two are joined: the browser ends up holding a one-time token, the app redeems it, and the answers and the purchase become that subscriber's.
The token
When a visitor pays through a web funnel's checkout, the server mints a
claim token: 32 random bytes
(apps/api/src/services/funnel/token.ts:9-12), stored only as a hash —
the plaintext is discarded on the spot everywhere except the winner's
single response (apps/api/src/services/funnel/claim-email.ts:12-14).
It is valid for 7 days
(apps/api/src/services/funnel/complete-purchase.ts:43) and redeemable
once: redeeming it again from the same subscriber returns the same
payload without running the merge twice
(apps/api/src/routes/v1/funnel-claim.ts:458-464); from a different
subscriber it fails with 409 Token already claimed
(apps/api/src/routes/v1/funnel-claim.ts:465). An expired token answers
410 (apps/api/src/routes/v1/funnel-claim.ts:436-438).
Getting it to the app
Set a deep link scheme in the funnel's settings and the success screen
offers a link that opens your app directly. The link carries the token and
the project (packages/shared/src/funnel/claim-link.ts:25-26,
apps/api/src/services/funnel/claim-links.ts:44-46):
yourscheme://onboarding-complete?token=<token>&project=<projectId>Handle that URL in your app and pass the token to claimFunnelToken. The
/confirm response hands this link to the success screen alongside the
token (apps/api/src/services/funnel/claim-email.ts:9-10).
Set a universal link domain and the success screen offers an https
link instead: https://<domain>/universal/funnels/open/<token>
(packages/shared/src/funnel/claim-link.ts:22,
packages/shared/src/funnel/claim-link.ts:29-31). The link is served by
the API under /universal
(apps/api/src/app.ts:261,
apps/api/src/routes/public/funnel-universal.ts:52-53), so the domain has
to reach the API.
What the landing does depends on who opens it: an Android user agent
redirects to the Play Store with the install referrer carrying the token,
an iOS one stashes a deferred fingerprint row and redirects to the App
Store, and anything else gets a page with both store links
(apps/api/src/routes/public/funnel-universal.ts:87-92,
apps/api/src/routes/public/funnel-universal.ts:94-121,
apps/api/src/routes/public/funnel-universal.ts:124-128).
If the app was not installed yet, there is nothing to open. After a fresh install the SDK can ask whether a recent visit belongs to this device:
- Android reads the Play install referrer, which the universal-link
landing wrote (
apps/api/src/routes/public/funnel-universal.ts:7-9). The referrer is authoritative: a token in it is looked up and handed straight back (apps/api/src/routes/v1/funnel-claim.ts:557-569). - iOS matches on a hashed IP, and only when exactly one unclaimed
visit matches — never a device fingerprint, and never a guess between
two (
apps/api/src/routes/v1/funnel-claim.ts:572-586).
The window is one hour from the visit
(apps/api/src/routes/public/funnel-universal.ts:50). The success
screen's CTA also writes a clipboard marker the app can read on first
launch — the deferred handoff for a fresh install
(apps/dashboard/src/runner/clipboard.ts:2-10,
apps/dashboard/src/runner/funnel-runner.tsx:1356-1361).
For a buyer who closed the tab. Two ways in:
Rovenue emails them. When the Stripe webhook completes the purchase
first — a closed tab, a phone that slept — the token is minted with nobody
there to receive it, and the buyer is emailed the same link the success
screen would have shown
(apps/api/src/services/funnel/claim-email.ts:9-19,
apps/api/src/services/stripe/stripe-webhook.ts:484-496). This needs the
funnel to have a deep link or a universal link configured; without one
there is nowhere to send them, and no email goes out
(apps/api/src/services/funnel/claim-email.ts:40-49).
The app asks. Your app can also ask Rovenue to look up an email; if
there is a match, Rovenue emails a magic link that opens the app. That
call always answers 202 with a null body whether or not a match existed,
so it cannot be used to test which addresses have bought
(apps/api/src/routes/v1/funnel-claim.ts:610-614,
apps/api/src/routes/v1/funnel-claim.ts:633-638).
This only works for funnels that took payment on the page — the
payment-intent request requires the email, and only its keyed hash is ever
stored, never the plaintext
(apps/api/src/routes/public/funnel-payment.ts:65-69,
apps/api/src/routes/public/funnel-payment.ts:1109-1118).
Redeeming it
let token = "<token from the opened URL>"
let result = try await Rovenue.shared.claimFunnelToken(token)
print(result.subscriberId)
print(result.funnelAnswersJson)Or observe claims resolved by any path on Rovenue.shared.funnelClaims
(packages/sdk-swift/Sources/Rovenue/Rovenue.swift:949).
val token = "<token from the opened URL>"
val result = Rovenue.shared.claimFunnelToken(token)
println(result.subscriberId)Rovenue.shared is the configured instance
(packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/Rovenue.kt:206-208);
funnelClaims streams claims resolved by any path
(packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/Rovenue.kt:905).
import { Rovenue } from "@rovenue/react-native-sdk";
const openedUrl = "<the URL that opened the app>";
const token = "<token from the opened URL>";
// One call that tries the lot: the opened URL, then the deferred match.
const claim = await Rovenue.resolveFunnelClaim({ url: openedUrl });
// Or redeem a token you already have:
const result = await Rovenue.claimFunnelToken(token);resolveFunnelClaim runs the no-gesture chain — the deep link, then the
platform deferred match — while the clipboard and the email link stay
gesture fallbacks you surface yourself
(packages/sdk-rn/src/api/funnel.ts:141-157).
const token = '<token from the opened URL>';
const openedUrl = '<the URL that opened the app>';
final result = await Rovenue.instance.claimFunnelToken(token);
// Or let the SDK read the token out of the URL that opened the app:
final claim = await Rovenue.instance.claimFromUrl(openedUrl);(packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:29,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:299,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:337-340).
What you get back
The claim payload carries three fields
(apps/api/src/routes/v1/funnel-claim.ts:349-356,
apps/api/src/routes/v1/funnel-claim.ts:390):
| Field | What it is |
|---|---|
subscriber_id | The subscriber this device now is |
entitlements | The live entitlement identifiers after the merge |
funnel_answers | One flat map of question id to the recorded value |
funnel_answers is built from the session's stored answers
(apps/api/src/routes/v1/funnel-claim.ts:357-365). entitlements is the
live access set — active and unexpired, translated to catalog identifiers
by the same rule the entitlements endpoint uses
(apps/api/src/routes/v1/funnel-claim.ts:367-388) — and it is read after
the claim transaction commits, because only then can it see the merge
(apps/api/src/routes/v1/funnel-claim.ts:341-347). The SDK result types
surface the subscriber and the answers
(packages/sdk-rn/src/api/funnel.ts:4-7,
packages/sdk-swift/Sources/Rovenue/Generated/RovenueFFI.swift:2105-2107).
What happens to the subscriber
A buyer who paid on the web has no install yet, so the purchase is held on
a placeholder subscriber keyed by the Stripe customer
(apps/api/src/services/funnel/complete-purchase.ts:384-389). Claiming
merges it into the device's subscriber — purchases, revenue, entitlements
and credit balances move across, and the placeholder is retired — inside
the same transaction that spends the token, so it cannot happen twice
(apps/api/src/routes/v1/funnel-claim.ts:476-493). The claim also flips
the session to completed and emits funnel.session.completed with
funnel.claim_token.claimed
(apps/api/src/routes/v1/funnel-claim.ts:494-520).
claimFunnelToken, claimInstall and claimViaEmail are members of the
SDK object on Swift, Kotlin, React Native and Flutter
(packages/sdk-swift/Sources/Rovenue/Rovenue.swift:972,
packages/sdk-swift/Sources/Rovenue/Rovenue.swift:982,
packages/sdk-swift/Sources/Rovenue/Rovenue.swift:992,
packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/Rovenue.kt:851-852,
packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/generated/librovenue.kt:1389-1391,
packages/sdk-rn/src/index.ts:116-121,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:299,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:305-311). The
helpers that read a token out of a URL or the clipboard —
claimFromUrl, claimFromClipboard, plus a bare extractFunnelToken —
exist on React Native and Flutter
(packages/sdk-rn/src/api/funnel.ts:67-71,
packages/sdk-rn/src/api/funnel.ts:93-135,
packages/sdk-rn/src/index.ts:147,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:319,
packages/sdk-flutter/rovenue_flutter/lib/src/rovenue.dart:337-340), and
resolveFunnelClaim, which tries every path in turn, is React Native
only (packages/sdk-rn/src/api/funnel.ts:151-157). The web SDK's surface
is client, storage, cache, events and configure — it has no claim
entrypoint (packages/sdk-web/src/index.ts:7-11,
packages/sdk-web/src/index.ts:122).