Rovenue SDK
FunnelsWeb

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

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):

FieldWhat it is
subscriber_idThe subscriber this device now is
entitlementsThe live entitlement identifiers after the merge
funnel_answersOne 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).

On this page