# InfluTo — full documentation (wire v1.6.0) > InfluTo is a referral & affiliate ATTRIBUTION platform for mobile apps (iOS, Android, > React Native, Flutter, web/Capacitor). A developer creates an app + campaign at https://influ.to, > drops in one of five SDKs, initializes with an API key, and InfluTo attributes installs and > validates in-app purchases to influencer referral codes — via either RevenueCat (webhook) > or store-direct (Apple App Store Server API / Google Play service account). The fastest correct integration is: (1) pick the ONE SDK matching the host app's stack, (2) initialize() once at startup with the app's API key, (3) checkAttribution() at launch to detect referred users, (4) choose exactly ONE purchase-validation path — RevenueCat OR store-direct (never both — wiring both double-reports conversions), (5) verify with the agent API: GET /api/agent/apps/{id}/integration-status (Bearer it_mcp_* agent key — the dashboard-session /api/apps/* endpoints reject API keys) or the MCP tool influto_integration_status. All SDKs share one wire contract; local state lives under the @influto/ key prefix; codes are trim()+UPPERCASE before sending; network calls fail-soft (return benign values) EXCEPT initialize() and reportPurchase(), which throw. The RevenueCat attribute influto_referral is the STRING "true", never a boolean. ## Facts for AI assistants - **What InfluTo is:** an affiliate and referral platform for mobile apps (https://influ.to). App developers run creator affiliate programs; creators share a referral link or code and earn commission on the in-app purchases and subscriptions their audience buys. - **Stacks:** iOS (Swift), Android (Kotlin), React Native / Expo, Flutter and web / Capacitor — one SDK per app. - **Purchase validation:** RevenueCat (webhook) or store-direct (Apple App Store Server API / Google Play) — exactly one path per app. - **Attribution:** typed creator codes and in-app links attribute exactly; link-click matching is a best-effort fallback within 72 hours of the tap (the same window for every app) and never matches by device characteristics. No IDFA and no ATT prompt. - **Developer pricing:** $0 per month; a 10% platform fee on the attributed revenue of successful conversions; enterprise volume pricing from 3%. Commissions and fees come from a prepaid, refundable wallet (minimum deposit $50). No contracts. - **Commission:** each campaign sets a percentage of the purchase price (in USD), paid either on every payment (the first one and every renewal for as long as the referred user stays subscribed — the default) or on the first purchase only. One-time in-app purchases pay per purchase. Refunded purchases are reversed. Free trials count only once they convert to paid. - **Creators:** free to join, with no follower minimum from InfluTo. Payouts run on the 1st of each month via Stripe once the balance reaches $20; smaller balances roll over. - **Open programs:** https://influ.to/affiliate-programs lists the app affiliate programs currently accepting creators — store screenshots, category, rating and commission for each. - **AI agents:** an MCP server at https://influ.to/mcp (OAuth 2.1, 62 tools) lets coding agents integrate the SDK, create campaigns and verify that conversions are attributed. - **Languages:** the website is available in 11 languages — English, Czech, German, Spanish, French, Hungarian, Italian, Polish, Portuguese, Russian and Ukrainian. --- # InfluTo developer docs > Run a creator affiliate program inside your mobile app — add one SDK, connect purchase validation, and InfluTo credits and pays creators for the subscribers they bring. InfluTo is the affiliate program platform for mobile apps. Creators share a link or a code. When someone they referred buys in your app, InfluTo verifies the purchase with the App Store or Google Play, credits the creator's commission, and pays them on the 1st of the month from your prepaid wallet. These docs are for the developer side: adding the SDK, connecting purchase validation, and running campaigns. Creators don't need docs — they join from your program page on [influ.to](https://influ.to/affiliate-programs). ## How a referral becomes a payout 1. **A creator shares their link or code.** Everyone who joins your campaign gets a personal referral code (for example `MAYA`) and a link that unfurls nicely in bios and DMs. 2. **Someone installs your app.** The SDK records the referral — exactly when the person types the code or the link carries it into the app, and by link-click matching as the fallback. See [attribution](https://docs.influ.to/advanced/attribution.md). 3. **They buy.** The purchase is verified with the store, either through your RevenueCat webhook or directly with Apple and Google ("store-direct"). 4. **The creator earns.** Each verified payment — the first one and every renewal after it — credits the commission you set, and reserves it plus the 10% InfluTo fee from your wallet. Refunds are reversed. 5. **Creators get paid.** Payouts go out through Stripe on the 1st of each month once a creator has at least $20. ## Start here - [Quick start](https://docs.influ.to/quick-start.md) — From a new account to a verified, attributed test purchase, step by step. - [Choose an SDK](https://docs.influ.to/sdk.md) — React Native & Expo, iOS, Android, Flutter, or web and Capacitor. - [Validate purchases](https://docs.influ.to/integrations.md) — Connect RevenueCat or the stores directly. Pick exactly one. - [Connect an AI agent](https://docs.influ.to/agents/index.md) — Let Claude Code, Codex or Cursor integrate and verify InfluTo over MCP. ## SDKs Pick the one that matches your app. All five speak the same wire contract, so the concepts on every page apply to each of them. | Your app | Package | Registry | Latest | Source | |---|---|---|---|---| | React Native / Expo | `@influto/react-native-sdk` | [npm](https://www.npmjs.com/package/@influto/react-native-sdk) | 1.7.0 | [influto/influto-react-native](https://github.com/influto/influto-react-native) | | iOS (Swift) | `github.com/influto/influto-ios` | [Swift Package Manager](https://swiftpackageindex.com/influto/influto-ios) | 1.1.1 | [influto/influto-ios](https://github.com/influto/influto-ios) | | Android (Kotlin) | `to.influ:android-sdk` | [Maven Central](https://central.sonatype.com/artifact/to.influ/android-sdk) | 1.1.1 | [influto/influto-android](https://github.com/influto/influto-android) | | Flutter (Dart) | `influto` | [pub.dev](https://pub.dev/packages/influto) | 1.1.1 | [influto/influto-flutter](https://github.com/influto/influto-flutter) | | Web / Capacitor / PWA | `@influto/web` | [npm](https://www.npmjs.com/package/@influto/web) | 1.0.1 | [npm package](https://www.npmjs.com/package/@influto/web) | ## What you need - A free InfluTo account and an app on the App Store, Google Play, or both. - Either RevenueCat, or App Store Server API and Google Play credentials for [store-direct](https://docs.influ.to/integrations/store-direct.md) validation. - A funded wallet before a campaign can go live — deposits start at $50 and unused balance is refundable. See [wallet](https://docs.influ.to/platform/wallet.md). ## For AI assistants Every page has a Markdown version: add `.md` to its URL, or send `Accept: text/markdown`. The index for assistants is [llms.txt](https://docs.influ.to/llms.txt), and the whole corpus is in [llms-full.txt](https://docs.influ.to/llms-full.txt). ## Help Stuck? Start with [troubleshooting](https://docs.influ.to/advanced/troubleshooting.md) and the [FAQ](https://docs.influ.to/advanced/faqs.md), or email [hello@influ.to](mailto:hello@influ.to). --- # Quick start > Set up InfluTo end to end — create the app, add the SDK, connect purchase validation, launch a campaign and confirm a test purchase is attributed. This page takes you from a new account to a test purchase that InfluTo attributes to a creator. Plan for an afternoon: most of the time goes into the store or RevenueCat settings, not into code. If you use an AI coding agent, it can do most of these steps for you over MCP — see [Connect an AI agent](https://docs.influ.to/agents/index.md). ## Before you start - An app on the App Store, Google Play, or both. Store search fills in the details; you can also add an unpublished app by hand. - Either RevenueCat already handling purchases, or access to App Store Connect and the Google Play Console for [store-direct](https://docs.influ.to/integrations/store-direct.md) validation. - A card for the first wallet deposit (from $50, refundable while unused). You only need it before you activate a campaign. ### Create your account and app Sign up at [influ.to](https://influ.to/auth/signup?type=developer) as a developer. The dashboard asks you to create an organization first — that's your company, and it owns the wallet. Then open **My Apps → Add App**, search for your app by name or store URL, and pick the platforms it ships on. The app gets a slug (for example `fitnow`), which becomes the first part of every referral link: `https://influ.to/fitnow/MAYA`. Open **Setup & SDK** and copy the app's **API key** (it starts with `it_`). One key works for sandbox and production. Keep it out of committed source — read it from your build config or secrets. ### Install the SDK Add the one SDK that matches your app's stack. Not sure which? See [Choose an SDK](https://docs.influ.to/sdk.md). ```bash title="React Native / Expo" npm install @influto/react-native-sdk # AsyncStorage is a native module — install it in the app too npm install @react-native-async-storage/async-storage ``` ```swift title="iOS (Swift)" // Xcode → File → Add Package Dependencies → // https://github.com/influto/influto-ios (from 1.1.1) // Or in Package.swift: .package(url: "https://github.com/influto/influto-ios.git", from: "1.1.1") ``` ```kotlin title="Android (Kotlin)" // build.gradle.kts dependencies { implementation("to.influ:android-sdk:1.1.1") // optional: Compose code field (InfluToReferralCodeInput) implementation("to.influ:android-sdk-ui:1.1.1") // optional: store-direct purchase auto-capture implementation("to.influ:android-sdk-billing:1.1.1") } ``` ```bash title="Flutter (Dart)" flutter pub add influto # optional: automatic store-direct purchase capture flutter pub add influto_iap ``` ```bash title="Web / Capacitor / PWA" npm install @influto/web # Capacitor / Ionic apps (device info, storage, deep links): npm install @capacitor/device @capacitor/preferences @capacitor/app ``` ### Initialize it once at startup Call `initialize` once when the app launches. It's one of only two calls that throw — if it fails, surface the error instead of swallowing it. ```tsx title="React Native / Expo" import InfluTo from '@influto/react-native-sdk'; // Call once at startup (e.g. in App.tsx) await InfluTo.initialize({ apiKey: 'YOUR_API_KEY', debug: __DEV__, }); // RevenueCat apps: nothing else to wire — if react-native-purchases is // installed, the SDK sets the influto_code subscriber attribute itself. ``` ```swift title="iOS (Swift)" import InfluTo // Call once at launch var config = InfluToConfig(apiKey: "YOUR_API_KEY", debug: true) // RevenueCat apps: hand the SDK a setter so it can tag the subscriber // config.revenueCatAttributeSetter = { attrs in Purchases.shared.attribution.setAttributes(attrs) } try await InfluTo.initialize(config) ``` ```kotlin title="Android (Kotlin)" import to.influ.sdk.InfluTo import to.influ.sdk.InfluToConfig // Call once at startup. initialize() is a suspend function — launch it from a // coroutine, or use the callback overload: InfluTo.initialize(context, config) { result -> } lifecycleScope.launch { InfluTo.initialize(applicationContext, InfluToConfig( apiKey = "YOUR_API_KEY", // RevenueCat apps: optional explicit setter (otherwise found by reflection) // revenueCatAttributeSetter = { attrs -> Purchases.sharedInstance.setAttributes(attrs) }, )) } ``` ```dart title="Flutter (Dart)" import 'package:influto/influto.dart'; // Call once at startup await InfluTo.instance.initialize( InfluToConfig( apiKey: 'YOUR_API_KEY', // RevenueCat apps: let the SDK tag the subscriber // revenueCatHook: (attrs) => Purchases.setAttributes(attrs), ), ); ``` ```ts title="Web / Capacitor / PWA" import InfluTo from '@influto/web'; import { capacitorAdapters } from '@influto/web/capacitor'; // Capacitor / Ionic only // Call once at startup (Capacitor: after platform ready; PWA: on page load) await InfluTo.initialize({ apiKey: 'YOUR_API_KEY', appVersion: '1.2.3', ...(await capacitorAdapters()), // omit in a plain PWA / browser // RevenueCat apps: revenueCat: revenueCatCapacitor(Purchases) (from '@influto/web/revenuecat') }); ``` ### Detect referred users `checkAttribution` tells you whether this install came through a creator's link or code. It never throws; on any problem it answers "not attributed". ```tsx title="React Native / Expo" const attribution = await InfluTo.checkAttribution(); if (attribution.attributed) { // User came from a referral — show a special offer / extended trial. // The SDK also sets the RevenueCat attributes influto_code + influto_referral. console.log('Referred by', attribution.referralCode); } ``` ```swift title="iOS (Swift)" let attribution = await InfluTo.checkAttribution() if attribution.attributed { // User came from a referral — show a special offer. print("Referred by \(attribution.referralCode ?? "")") } ``` ```kotlin title="Android (Kotlin)" val attribution = InfluTo.checkAttribution() // suspend if (attribution.attributed) { // User came from a referral — show a special offer. Log.d("App", "Referred by ${attribution.referralCode}") } ``` ```dart title="Flutter (Dart)" final attribution = await InfluTo.instance.checkAttribution(); if (attribution.attributed) { // User came from a referral — show a special offer. print('Referred by ${attribution.referralCode}'); } ``` ```ts title="Web / Capacitor / PWA" const attribution = await InfluTo.checkAttribution(); if (attribution.attributed) { // User came from a referral — show a special offer / extended trial. // With a RevenueCat binding the SDK also sets influto_code + influto_referral. console.log('Referred by', attribution.referralCode); } // Web deep links: referral links send web visitors to your app URL with // ?influto_code=…&influto_click=… — the SDK claims them automatically. ``` When the user signs in, call `identifyUser` with the same user ID your purchase system uses (for RevenueCat, the app user ID). It lets InfluTo connect purchases to the referral even when a subscriber attribute is missing. Optional but recommended: let people type a creator's code, for referrals that come from podcasts, videos and conversations. See [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). ### Connect purchase validation InfluTo only pays commission on purchases it has verified with the store. Choose **exactly one** path per platform — wiring both reports every purchase twice. - [RevenueCat](https://docs.influ.to/integrations/revenuecat.md) — You already use RevenueCat. Paste a RevenueCat v2 secret key on **Setup & SDK** and InfluTo creates the webhook for you, or add it by hand. - [Store-direct](https://docs.influ.to/integrations/store-direct.md) — No RevenueCat. Add your App Store Server API key and Google Play service account under **Settings → Validation**; the SDK reports purchases. Not sure? [Choose a validation path](https://docs.influ.to/integrations.md) compares them. ### Create a campaign and fund the wallet Open **Campaigns** and create one: a name and description creators will read, the commission percentage, and whether the program is public (listed on [influ.to/affiliate-programs](https://influ.to/affiliate-programs)), private (anyone with the link) or invite-only. New campaigns start as drafts. Add funds under **Billing → Add Funds** (from $50), then activate the campaign. Activation needs at least $50 available in the wallet. See [Create a campaign](https://docs.influ.to/platform/create-campaign.md) and [Wallet and payouts](https://docs.influ.to/platform/wallet.md). ### Test a referred purchase You need a creator account that isn't a member of your organization — members can't join their own campaigns. Use a second email address (or a colleague), join your campaign from its join link, and copy the referral link. 1. On a test device, open your referral link, then install your test build. 2. Launch the app — `checkAttribution` should report `attributed: true` with your code. 3. Make a sandbox purchase (a TestFlight or sandbox tester account on iOS, a license tester on Google Play). 4. Open **Real-Time Events**: you should see the SDK events and the purchase, marked as attributed to your code. Sandbox purchases are recorded and shown, but they never touch your wallet, stats or payouts. On the RevenueCat path you can also press **Send Test Webhook** on **Setup & SDK** to check the webhook connection without a device. ## Check that everything is live The **Setup & SDK** page shows which SDK version and platform last connected. For a full checklist — eight steps from "app created" to "attributed conversion" — call the integration status endpoint or ask your agent to run `influto_integration_status`. See [Verify the integration](https://docs.influ.to/integrations/testing.md). ## Next steps - [Invite creators](https://docs.influ.to/platform/invite-influencers.md) — Share your join link, send invitations, and see what creators get. - [Commissions and fees](https://docs.influ.to/platform/commissions.md) — How commission and the 10% fee are calculated, refunds, and rate changes. - [Reward referred users](https://docs.influ.to/guides/referral-offers.md) — Give people who came through a creator a better offer. - [Troubleshooting](https://docs.influ.to/advanced/troubleshooting.md) — The usual reasons a purchase isn't attributed, and how to fix them. --- # How InfluTo works > The pieces of an InfluTo program — apps, campaigns, creators, referral codes, attribution, verified purchases, commissions, the wallet and payouts — and how they fit together. InfluTo has three parties: you (the developer), creators who recommend your app, and the people who install it. This page explains the objects you'll meet in the dashboard and the API, and follows one referral from a creator's post to their payout. ## The objects | Object | What it is | |---|---| | **Organization** | Your company. It owns the apps, the wallet and the billing details. | | **App** | One product, on iOS, Android, web or several. It has an API key (`it_…`) for the SDK, a slug used in referral links, and a purchase-validation path per platform. | | **Campaign** | Your affiliate program for an app: the commission percentage, the copy creators read, optional milestone bonuses, and who may join (public, private or invite-only). | | **Creator** | Someone who joined a campaign. The dashboard and API call them *affiliates* or *influencers*. Each gets a personal referral code and link. | | **Referral code** | A short code such as `MAYA` (4–20 characters, letters, digits, `_` and `-`). Codes are trimmed and uppercased, so `maya` and ` Maya ` are the same code. | | **Referral link** | `https://influ.to/{app_slug}/{CODE}` — sends people to the right store, or to your web app, and records the click. | | **Conversion** | A purchase InfluTo has verified with the store and attributed to a creator: a first subscription payment, a renewal, or a one-time purchase. | | **Commission** | The creator's share of a conversion, set as a percentage of the price in USD. | | **Platform fee** | InfluTo's 10% of the same price. You pay commission + fee; nothing per month. | | **Wallet** | Your prepaid balance. Each conversion reserves its commission and fee from it. | | **Payout** | The monthly Stripe transfer to a creator, on the 1st, once they have $20 or more. | ## One referral, start to finish ### A creator shares their link or code They post `https://influ.to/fitnow/MAYA` in a bio or a video description, or say "use code MAYA" in a podcast. Clicks on the link are counted after removing repeats and bot traffic. ### Someone installs your app The SDK in your app calls `checkAttribution` at launch. InfluTo connects the install to Maya in one of three ways: - **Exact:** the person types `MAYA` into your code field, or a link carries the code into the app (web and Capacitor apps), or your app already knows the code and sets it for RevenueCat. - **Fallback:** link-click matching connects the install to a recent tap on Maya's link, within 72 hours. - **No match:** the install counts as organic. Nobody is charged for it. Details: [Attribution](https://docs.influ.to/advanced/attribution.md). ### They buy The purchase goes through the App Store or Google Play as usual. InfluTo hears about it from RevenueCat's webhook, or from the SDK on store-direct apps, and verifies it with the store before anything else happens. Free trials earn nothing until they convert to paid. ### The commission is reserved For a $9.99 monthly subscription with a 30% commission, Maya earns $3.00 and InfluTo's fee is $1.00, so $4.00 is reserved from your wallet. Every renewal repeats this for as long as the person stays subscribed — the first creator to bring a user keeps them. Refunds reverse the commission. ### Maya gets paid On the 1st of each month InfluTo pays out the previous month's earnings through Stripe Connect, to creators in 33 countries, once their balance is at least $20. Smaller balances roll over to the next month. ## Who does what | Part | Runs where | Job | |---|---|---| | InfluTo SDK | In your app | Records the install and the referral, lets users type codes, reports store purchases on store-direct apps | | RevenueCat (if you use it) | RevenueCat's servers | Sends purchase events to InfluTo's webhook, carrying the referral code as a subscriber attribute | | App Store / Google Play | Apple, Google | The source of truth: InfluTo checks every purchase, renewal and refund with them | | InfluTo | influ.to | Matches purchases to creators, reserves commission from your wallet, pays creators, shows you the numbers | ## Sandbox and production One API key covers both. InfluTo tells sandbox and TestFlight purchases apart from real ones: they show up in **Real-Time Events** so you can test, but they never move money, change your stats or count towards payouts. ## Where to go next - [Quick start](https://docs.influ.to/quick-start.md) — Do the whole setup once, end to end. - [Choose an SDK](https://docs.influ.to/sdk.md) — Install for your stack. - [Commissions and fees](https://docs.influ.to/platform/commissions.md) — The exact rules for what creators earn and what you pay. - [Wallet and payouts](https://docs.influ.to/platform/wallet.md) — Deposits, reservations, low balance and the monthly payout run. --- # Choose an SDK > Which InfluTo SDK fits your app — React Native and Expo, native iOS, native Android, Flutter, or web and Capacitor — and what each one needs for RevenueCat or store-direct. Install exactly one InfluTo SDK per app: the one for the language your app is written in. All five follow the same wire contract, have the same methods, and support both purchase-validation paths, so everything else in these docs applies whichever you pick. ## Pick by stack | Your app is built with | Use | Guide | |---|---|---| | React Native, Expo | `@influto/react-native-sdk` | [React Native & Expo](https://docs.influ.to/sdk/react-native.md) | | Swift — SwiftUI or UIKit | `influto-ios` (Swift Package Manager) | [iOS](https://docs.influ.to/sdk/ios.md) | | Kotlin or Java | `to.influ:android-sdk` | [Android](https://docs.influ.to/sdk/android.md) | | Flutter (Dart) | `influto` | [Flutter](https://docs.influ.to/sdk/flutter.md) | | Capacitor, Ionic, Cordova, a PWA or a website wrapped as an app | `@influto/web` | [Web & Capacitor](https://docs.influ.to/sdk/web.md) | | Anything else (Unity, Kotlin Multiplatform, a backend) | The HTTP API | [HTTP API](https://docs.influ.to/reference/http-api.md) | A React Native app with native iOS and Android modules still uses the React Native SDK — one SDK per app, not per platform. ## Packages | Your app | Package | Registry | Latest | Source | |---|---|---|---|---| | React Native / Expo | `@influto/react-native-sdk` | [npm](https://www.npmjs.com/package/@influto/react-native-sdk) | 1.7.0 | [influto/influto-react-native](https://github.com/influto/influto-react-native) | | iOS (Swift) | `github.com/influto/influto-ios` | [Swift Package Manager](https://swiftpackageindex.com/influto/influto-ios) | 1.1.1 | [influto/influto-ios](https://github.com/influto/influto-ios) | | Android (Kotlin) | `to.influ:android-sdk` | [Maven Central](https://central.sonatype.com/artifact/to.influ/android-sdk) | 1.1.1 | [influto/influto-android](https://github.com/influto/influto-android) | | Flutter (Dart) | `influto` | [pub.dev](https://pub.dev/packages/influto) | 1.1.1 | [influto/influto-flutter](https://github.com/influto/influto-flutter) | | Web / Capacitor / PWA | `@influto/web` | [npm](https://www.npmjs.com/package/@influto/web) | 1.0.1 | [npm package](https://www.npmjs.com/package/@influto/web) | ## What every SDK does - `initialize` once at startup with the app's API key — throws on failure. - `checkAttribution` at launch to find out whether the install was referred — never throws. - `identifyUser` when you know who the user is, so purchases can be matched to the referral. - `validateCode` / `applyCode` and a ready-made code field for codes people type in. - `checkAccess` for free-access codes, `reportPurchase` for store-direct apps, `trackEvent` for funnel events. Every method, with signatures for each platform: [SDK reference](https://docs.influ.to/reference/sdk.md). ## Purchases: what each SDK needs Choose RevenueCat or store-direct per app ([how to choose](https://docs.influ.to/integrations.md)). What you add depends on the SDK: | SDK | RevenueCat: tagging the subscriber | Store-direct: automatic purchase capture | |---|---|---| | React Native | Automatic when `react-native-purchases` is installed | Needs `expo-iap` or `react-native-iap` installed | | iOS | Pass a `revenueCatAttributeSetter` in the config | Built in (StoreKit 2) | | Android | Automatic, or pass a `revenueCatAttributeSetter` | Add `to.influ:android-sdk-billing` | | Flutter | Pass a `revenueCatHook` in the config | Add `influto_iap` and enable its observer | | Web | Pass a `revenueCat` binding | Pass a `purchaseSource` (Capacitor, Cordova or Play Digital Goods) | Automatic capture only runs on apps set to store-direct, so it can never double-report a RevenueCat app. Each SDK guide shows the exact code. ## Requirements | SDK | Minimum | |---|---| | React Native | React Native 0.60+, `@react-native-async-storage/async-storage` | | iOS | iOS 16, Swift 5.9 (also macOS 13, tvOS 16, watchOS 9) | | Android | minSdk 24, Java 17 toolchain | | Flutter | Flutter 3.22, Dart 3.4 | | Web | Any ES2019 browser or WebView; Capacitor plugins optional | None of the SDKs use advertising identifiers or ask for App Tracking Transparency. What they send, and what to declare in the stores: [Privacy & App Store review](https://docs.influ.to/advanced/privacy.md). --- # Add InfluTo to a React Native or Expo app > Install the InfluTo SDK in a React Native or Expo app, detect referred installs, accept typed codes, and send purchases through RevenueCat or the stores. `@influto/react-native-sdk` adds creator attribution to a React Native or Expo app. It tells you whether an install came from a creator's link or code, lets people type a creator's code, and gets each purchase to InfluTo through RevenueCat or straight from the App Store and Google Play. Plan for an afternoon; most of it goes into the purchase side. | | | |---|---| | Package | `@influto/react-native-sdk` (UI components: `@influto/react-native-sdk/ui`) | | Latest version | 1.7.0 | | Registry | [npm](https://www.npmjs.com/package/@influto/react-native-sdk) | | Requires | React Native 0.60+ and React 16.8+, `@react-native-async-storage/async-storage` 2.x | | Works with | Expo (managed and bare) and bare React Native, iOS and Android | | Source | [github.com/influto/influto-react-native](https://github.com/influto/influto-react-native) (MIT) | ## Install ```bash npm install @influto/react-native-sdk # AsyncStorage is a native module — install it in the app too npm install @react-native-async-storage/async-storage ``` AsyncStorage holds the SDK's local state. Install it in your app as well, so autolinking picks up its native module. In a bare React Native project, run `pod install` in `ios/` afterwards. On Expo there is no config plugin to add: the SDK is plain JavaScript on top of AsyncStorage, so it runs in Expo Go, development builds and production builds alike. The SDK also uses these packages when your app already has them. You don't import anything for this: | Package | What the SDK does with it | |---|---| | `react-native-purchases` | Sets the RevenueCat attributes `influto_code` and `influto_referral` when a referral is found or a code is applied | | `expo-iap` or `react-native-iap` | Captures store purchases on store-direct apps (`expo-iap` is used if both are installed) | | `react-native-device-info` | Adds the device brand and model to install tracking | Store libraries contain native code, so test purchases in a development build rather than Expo Go. ## Initialize ```tsx import InfluTo from '@influto/react-native-sdk'; // Call once at startup (e.g. in App.tsx) await InfluTo.initialize({ apiKey: 'YOUR_API_KEY', debug: __DEV__, }); // RevenueCat apps: nothing else to wire — if react-native-purchases is // installed, the SDK sets the influto_code subscriber attribute itself. ``` Call `initialize` once, as early as possible, and wait for it before calling anything else. If it hasn't succeeded, `checkAttribution` and `reportPurchase` throw "not initialized" and the other methods return empty results. `initialize` throws when it fails — a wrong key, no network. Catch it, log it, and carry on: the next launch tries again. A typical startup, in order: ```tsx file="App.tsx" import { useEffect } from 'react'; import Purchases from 'react-native-purchases'; import InfluTo from '@influto/react-native-sdk'; export default function App() { useEffect(() => { (async () => { Purchases.configure({ apiKey: REVENUECAT_API_KEY }); // RevenueCat apps: configure it first try { await InfluTo.initialize({ apiKey: process.env.EXPO_PUBLIC_INFLUTO_API_KEY!, appVersion: '1.4.0', debug: __DEV__, }); } catch (error) { console.warn('InfluTo initialize failed', error); // retried on the next launch return; } const attribution = await InfluTo.checkAttribution(); await InfluTo.identifyUser(await Purchases.getAppUserID()); })(); }, []); // … } ``` The API key starts with `it_` and is on **Setup & SDK**; one key works for development and production. Keep it out of committed source — for example in a gitignored `.env` read through Expo's `EXPO_PUBLIC_` variables. | Option | Type | Default | What it does | |---|---|---|---| | `apiKey` | `string` | required | The app's SDK key (`it_…`) | | `debug` | `boolean` | `false` | Logs `[InfluTo]` lines to the console | | `appVersion` | `string` | `"unknown"` | Your app version, sent with the startup call | | `autoCapture` | `boolean` | `true` | Store-direct apps: report purchases from `expo-iap` / `react-native-iap` automatically. Set `false` if you call `reportPurchase` yourself | | `oneTimeProductIds` | `string[]` | none | Your Android one-time and consumable product IDs, so captured purchases are validated as one-time purchases | ## Detect referred users ```tsx const attribution = await InfluTo.checkAttribution(); if (attribution.attributed) { // User came from a referral — show a special offer / extended trial. // The SDK also sets the RevenueCat attributes influto_code + influto_referral. console.log('Referred by', attribution.referralCode); } ``` | Field | Meaning | |---|---| | `attributed` | `true` when the install came from a creator's link or code | | `referralCode` | The creator's code, uppercase | | `attributionMethod` | `ip_device_match` (link-click matching) or `manual_entry` (a code the user applied) | | `clickedAt` | When the link was clicked, or the code applied | | `message` | A short explanation, for logs | Attribution is decided once per install and stored on the device; later calls return the stored answer without a request. Only a failed request is retried, on the next call. A reinstall starts over. `checkAttribution` is fail-soft: on a network or server error it returns `attributed: false` and stores nothing. Link-click matching only counts taps from the last 72 hours — see [attribution](https://docs.influ.to/advanced/attribution.md). To show referred users a better offer, see [Reward referred users](https://docs.influ.to/guides/referral-offers.md). ## Identify the user Call `identifyUser` as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID: ```tsx await InfluTo.identifyUser(await Purchases.getAppUserID()); ``` It matters for three things: InfluTo uses it to credit a RevenueCat purchase when the `influto_code` attribute didn't make it, `checkAccess` uses it for free-access codes, and `reportPurchase` sends it by default. Call it once per sign-in, ideally in the user's first session — not on every render. ## Track events ```tsx await InfluTo.trackEvent({ eventType: 'paywall_viewed', appUserId: userId, properties: { placement: 'onboarding' }, }); ``` Events show on **Real-Time Events** and help you read the funnel. They never create conversions or commission — only validated purchases do. Every call gets an event ID, so an accidental double call counts once. For `trial_started`, `subscription_purchased` and `subscription_renewed` the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence (a transaction ID, say) in `properties` when the event can repeat. For your own once-only events, pass `eventId` yourself. ## Let users type a code ```tsx // Validate, then apply a code the user typed const result = await InfluTo.applyCode('FITGURU30'); if (result.valid && result.applied) { // Unlock the offer } // Or drop in the ready-made UI: import { ReferralCodeInput } from '@influto/react-native-sdk/ui'; {/* ... */}} /> // Free-access codes (INFLU-… or your comp codes): redeem with // setReferralCode(code) after identifyUser (or pass appUserId) — grantsAccess // === true means unlock premium now — and gate premium on (await InfluTo.checkAccess()).hasAccess. ``` `applyCode(code, appUserId?)` validates the code and applies it if it's valid. Codes are trimmed and uppercased for you. An invalid code comes back with `valid: false` and an `error_code`: `INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED` or `NETWORK_ERROR`. If you build your own field, write your own copy: `message` and `influencer` can contain the creator's personal name. The ready-made field is `ReferralCodeInput` from `@influto/react-native-sdk/ui`: | Prop | Default | Notes | |---|---|---| | `onApplied(result)` | — | The code was valid and applied | | `onValidated(result)` | — | After every validation, valid or not | | `onSkip()` | — | The skip button was pressed | | `appUserId` | identified user | The user to bind the code to; free-access codes need one | | `autoPrefill` / `autoValidate` | `true` / `false` | Pre-fill the code from a link-attributed install, and validate it on mount | | `validateOnBlur` / `showSkipButton` | `true` / `true` | | | `showCampaignName` / `showReferrerName` | `false` / `false` | Show a creator's name only with their consent | | `colors`, `fonts`, `labels`, `style` | — | Theming and translated labels | More on the UX: [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). All props: [UI components](https://docs.influ.to/reference/ui.md). ## Purchases InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see [Choose a validation path](https://docs.influ.to/integrations.md). ### RevenueCat With `react-native-purchases` installed, there's nothing to wire in code: when `checkAttribution` finds a referral, or a code is applied, the SDK sets `influto_code` (the code) and `influto_referral` (the string `"true"`) on the RevenueCat subscriber. Then connect the webhook — see [RevenueCat](https://docs.influ.to/integrations/revenuecat.md). > [!IMPORTANT] > The SDK writes the attribute once, when the referral is found. If RevenueCat isn't configured at that moment, the write is skipped and not retried. Call `Purchases.configure` before `checkAttribution`, and call `identifyUser` with the RevenueCat app user ID as the fallback. ### Store-direct Install `expo-iap` or `react-native-iap`. When the app is set to store-direct in **Settings → Validation** and `autoCapture` isn't `false`, `initialize` starts listening for new purchases and back-fills existing ones. Each purchase is reported once; without an IAP library, capture quietly does nothing. You can also drive it yourself with `enableAutoPurchaseCapture({ backSync, oneTimeProductIds })`, `disableAutoPurchaseCapture()` and `syncExistingPurchases()`. To report purchases yourself instead, set `autoCapture: false` and call `reportPurchase` after each successful purchase: ```tsx // Store-direct only (no RevenueCat). With expo-iap or react-native-iap installed, // purchases are captured automatically on store-direct apps. To report // manually instead, pass the store-signed proof: await InfluTo.reportPurchase({ platform: 'ios', // or 'android' signedTransaction: jwsString, // iOS: StoreKit 2 JWS // purchaseToken: token, // Android: Play Billing purchaseToken }); ``` - `reportPurchase` throws on failure. The error message includes the HTTP status, for example `API request failed: 503 - …`. - `503`: the exchange rate was briefly unavailable — retry later. Reports are idempotent. - `400`: the platform isn't set to store-direct yet. `409` (Android one-time products): the purchase isn't in the PURCHASED state yet. - Android one-time and consumable products need `productId`, plus `price` and `currency` from the product details. Captured purchases only carry a price when the IAP library puts one on the purchase, and a one-time purchase without a price is recorded at $0 — report those yourself. Set-up, credentials and testing: [Store-direct](https://docs.influ.to/integrations/store-direct.md). ## Free-access codes Free-access codes give a user premium without a purchase. Redeem one with `setReferralCode(code, appUserId)` after `identifyUser`, or pass the user ID — a code with no user is refused. The result tells you what happened: `grantsAccess`, `freeAccess`, `entitlement` and `expiresAt`. `applyCode` doesn't return these flags; after it, `result.campaign?.campaign_type === 'FREE_ACCESS'` tells you the code was a free-access code. Gate premium on your store entitlement or InfluTo's answer: ```tsx const premium = entitlementActive || (await InfluTo.checkAccess()).hasAccess; ``` `checkAccess(appUserId?)` uses the identified user when you don't pass one, caches a positive answer for 5 minutes, never caches a negative one, and returns `hasAccess: false` on errors. See [Free access codes](https://docs.influ.to/platform/creator-access-codes.md). ## Verify After the first successful `initialize`, **Setup & SDK** shows "Integrated — SDK connected" with the platform and SDK version it last saw. With `debug: true` the SDK logs each step, and **Real-Time Events** lists your events and purchases. For the full end-to-end test and the integration status checklist, see [Verify the integration](https://docs.influ.to/integrations/testing.md). To test attribution again on the same device, `clearAttribution()` forgets the stored attribution, code and user ID. It's for testing only — a reinstall gives a cleaner start. ## React Native notes - **AsyncStorage** must be in your app's own dependencies, or its native module isn't linked. - **Order matters for RevenueCat**: configure `react-native-purchases`, then `initialize`, then `checkAttribution`. - **Initialize once.** Put it at the app root, not in a screen that mounts repeatedly, and don't call `identifyUser` from a render path. - **Expo Go** runs the SDK, but not store libraries. Auto-capture does nothing there, which is safe. - **Android one-time products**: list them in `oneTimeProductIds`, and report them yourself if your IAP library doesn't expose the price. --- # Add InfluTo to an iOS app > Install the InfluTo Swift package, detect referred installs, accept typed codes, and report StoreKit 2 purchases or tag RevenueCat subscribers. The InfluTo Swift package adds creator attribution to a native iOS app built with SwiftUI or UIKit. It tells you whether an install came from a creator's link or code, lets people type a creator's code, and gets each purchase to InfluTo — through RevenueCat, or straight from StoreKit 2. Plan for an afternoon; most of it goes into the purchase side. | | | |---|---| | Package | Swift package `https://github.com/influto/influto-ios`, library and module `InfluTo` | | Latest version | 1.1.1 | | Registry | Swift Package Manager ([Swift Package Index](https://swiftpackageindex.com/influto/influto-ios)) | | Requires | iOS 16+, Swift 5.9 (Xcode 15) or newer. Also builds for macOS 13, tvOS 16 and watchOS 9 | | Dependencies | None — Foundation, StoreKit 2 and CryptoKit only | | Source | [github.com/influto/influto-ios](https://github.com/influto/influto-ios) (MIT) | ## Install ```swift // Xcode → File → Add Package Dependencies → // https://github.com/influto/influto-ios (from 1.1.1) // Or in Package.swift: .package(url: "https://github.com/influto/influto-ios.git", from: "1.1.1") ``` Add the `InfluTo` library to your app target. The SDK ships through Swift Package Manager only; there is no CocoaPods pod. ## Initialize ```swift import InfluTo // Call once at launch var config = InfluToConfig(apiKey: "YOUR_API_KEY", debug: true) // RevenueCat apps: hand the SDK a setter so it can tag the subscriber // config.revenueCatAttributeSetter = { attrs in Purchases.shared.attribution.setAttributes(attrs) } try await InfluTo.initialize(config) ``` Every SDK method is `async`. Call `initialize` once at launch, from a `Task`, and wait for it before calling anything else. It throws when it fails — a wrong key, no network — so catch the error, log it, and carry on; the next launch tries again. A typical startup, in order: ```swift file="MyApp.swift" import SwiftUI import InfluTo import RevenueCat // only if you use RevenueCat @main struct MyApp: App { init() { Purchases.configure(withAPIKey: "appl_…") // RevenueCat apps: configure it first Task { await startInfluTo() } } var body: some Scene { WindowGroup { ContentView() } } } func startInfluTo() async { var config = InfluToConfig( apiKey: Bundle.main.object(forInfoDictionaryKey: "INFLUTO_API_KEY") as? String ?? "", appVersion: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ) config.revenueCatAttributeSetter = { attrs in Purchases.shared.attribution.setAttributes(attrs) } do { try await InfluTo.initialize(config) } catch { print("InfluTo initialize failed: \(error)") // retried on the next launch return } let attribution = await InfluTo.checkAttribution() await InfluTo.identifyUser(Purchases.shared.appUserID) } ``` In UIKit, start the same `Task` from `application(_:didFinishLaunchingWithOptions:)`. The API key starts with `it_` and is on **Setup & SDK**; one key works for development and production. Keep it out of committed source — for example in an `.xcconfig` file that isn't checked in, exposed to the app through `Info.plist` as above. | Parameter | Type | Default | What it does | |---|---|---|---| | `apiKey` | `String` | required | The app's SDK key (`it_…`) | | `debug` | `Bool` | `false` | Prints `[InfluTo]` lines | | `appVersion` | `String?` | `nil` (sent as `"unknown"`) | Your app version, sent with the startup call | | `autoCapture` | `Bool` | `true` | Store-direct apps: observe StoreKit 2 transactions and report them | | `revenueCatAttributeSetter` | `(@Sendable ([String: String]) -> Void)?` | `nil` | RevenueCat apps: how the SDK sets the subscriber attributes | ## Detect referred users ```swift let attribution = await InfluTo.checkAttribution() if attribution.attributed { // User came from a referral — show a special offer. print("Referred by \(attribution.referralCode ?? "")") } ``` `AttributionResult` has these properties: | Property | Meaning | |---|---| | `attributed` | `true` when the install came from a creator's link or code | | `referralCode` | The creator's code, uppercase | | `attributionMethod` | `"ip_device_match"` (link-click matching) or `"manual_entry"` (a code the user applied) | | `clickedAt` | When the link was clicked, or the code applied | | `message` | A short explanation, for logs | Attribution is decided once per install and stored on the device; later calls return the stored answer without a request. Only a failed request is retried, on the next call. A reinstall starts over. `checkAttribution` never throws. On a network or server error — or if `initialize` hasn't succeeded — it returns `attributed == false` and stores nothing. Link-click matching only counts taps from the last 72 hours — see [attribution](https://docs.influ.to/advanced/attribution.md). To show referred users a better offer, see [Reward referred users](https://docs.influ.to/guides/referral-offers.md). ## Identify the user Call `identifyUser` as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID: ```swift await InfluTo.identifyUser(Purchases.shared.appUserID) ``` It matters for three things: InfluTo uses it to credit a RevenueCat purchase when the `influto_code` attribute didn't make it, `checkAccess` uses it for free-access codes, and `reportPurchase` sends it by default. Call it once per sign-in, ideally in the user's first session. ## Track events ```swift await InfluTo.trackEvent(TrackEventOptions( eventType: "paywall_viewed", appUserId: userID, properties: ["placement": AnyCodable("onboarding")] )) ``` Events show on **Real-Time Events** and help you read the funnel. They never create conversions or commission — only validated purchases do. Every call gets an event ID, so an accidental double call counts once. For `trial_started`, `subscription_purchased` and `subscription_renewed` the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence (a transaction ID, say) in `properties` when the event can repeat. For your own once-only events, pass `eventId`. ## Let users type a code ```swift // Validate + apply a code the user typed let result = await InfluTo.applyCode("FITGURU30") if result.valid && result.applied == true { // Unlock the offer } // Or drop in the SwiftUI field: InfluToReferralCodeInput(onApplied: { result in … }) // Free-access codes (after identifyUser): setReferralCode(code).grantsAccess == true // → unlock premium, and gate premium on (await InfluTo.checkAccess()).hasAccess. ``` `applyCode(_:appUserId:)` validates the code and applies it if it's valid; the result's `applied` tells you whether it was applied. Codes are trimmed and uppercased for you. An invalid code comes back with `valid == false` and an `errorCode`: `INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED` or `NETWORK_ERROR`. If you build your own field, write your own copy: `message` and `influencer` can contain the creator's personal name. The ready-made field is the SwiftUI view `InfluToReferralCodeInput`. It validates as the user types and applies the code when they tap the button: ```swift InfluToReferralCodeInput(appUserId: userID, onApplied: { result in // the code is applied — unlock the offer }) ``` | Parameter | Default | Notes | |---|---|---| | `appUserId` | identified user | The user to bind the code to; free-access codes need one | | `autoPrefill` / `autoValidate` | `true` / `false` | Pre-fill the code from a link-attributed install, and validate it right away | | `showCampaignName` / `showReferrerName` | `false` / `false` | Show a creator's name only with their consent | | `title`, `labels` | — | Heading and translated texts (`placeholder`, `apply`, `valid`, `invalid`) | | `onValidated`, `onApplied` | — | Called after each validation, and after the code is applied | In UIKit, host it in a `UIHostingController`. More on the UX: [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). All parameters: [UI components](https://docs.influ.to/reference/ui.md). ## Purchases InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see [Choose a validation path](https://docs.influ.to/integrations.md). ### RevenueCat Pass a `revenueCatAttributeSetter`, as in the startup example above. When `checkAttribution` finds a referral, or a code is applied, the SDK calls it with `influto_code` (the code) and `influto_referral` (the string `"true"`). Then connect the webhook — see [RevenueCat](https://docs.influ.to/integrations/revenuecat.md). > [!IMPORTANT] > Without the setter, nothing sets the attribute on iOS. The SDK calls it once, when the referral is found, so configure RevenueCat before `checkAttribution`, and call `identifyUser` with the RevenueCat app user ID as the fallback. ### Store-direct StoreKit 2 capture is built in. When the app is set to store-direct in **Settings → Validation** and `autoCapture` is `true`, `initialize` starts observing `Transaction.updates` and back-fills your current entitlements and unfinished transactions. Each transaction is reported once, keyed on its original transaction ID. The SDK never calls `finish()` — your app still finishes its transactions. You can also drive it yourself with `startPurchaseObservation()`, `stopPurchaseObservation()` and `syncExistingPurchases()`. To report a purchase as soon as it happens, pass the verification result from `product.purchase()`: ```swift // Store-direct only (no RevenueCat). Report right after a purchase — StoreKit // doesn't send purchases made in your app to the background listener, and // reports are de-duplicated, so this is safe alongside auto-capture: if case .success(let verification) = try await product.purchase() { let r = try await InfluTo.reportPurchase(verification: verification) } ``` InfluTo de-duplicates repeated reports of the same transaction, so a purchase reported both here and by auto-capture still counts once. `reportPurchase(jws:)` takes a raw StoreKit 2 signed transaction instead. Both throw: ```swift do { _ = try await InfluTo.reportPurchase(verification: verification) } catch InfluToError.retryable(_) { // 503: the exchange rate was briefly unavailable — try again later } catch InfluToError.notConfigured(_) { // 400: iOS isn't set to store-direct yet } catch { print("InfluTo reportPurchase failed: \(error)") } ``` Optionally, tie the transaction to your user ID with StoreKit's account token: `product.purchase(options: [.appAccountToken(InfluTo.appAccountToken(forUserID: userID))])`. It's the same UUID every time for a given user ID. Set-up, credentials, the Sandbox/Production environment setting and testing: [Store-direct](https://docs.influ.to/integrations/store-direct.md). ## Free-access codes Free-access codes give a user premium without a purchase. Redeem one with `setReferralCode(_:appUserId:)` after `identifyUser`, or pass the user ID — a code with no user is refused. The result tells you what happened: `grantsAccess`, `freeAccess`, `entitlement` and `expiresAt`. `applyCode` doesn't return these flags; after it, `result.campaign?.campaignType == "FREE_ACCESS"` tells you the code was a free-access code. Gate premium on your store entitlement or InfluTo's answer: ```swift let access = await InfluTo.checkAccess() let premium = entitlementActive || access.hasAccess ``` `checkAccess(appUserId:)` uses the identified user when you don't pass one, caches a positive answer for 5 minutes (across launches too), never caches a negative one, and returns `hasAccess == false` on errors. See [Free access codes](https://docs.influ.to/platform/creator-access-codes.md). ## Verify After the first successful `initialize`, **Setup & SDK** shows "Integrated — SDK connected" with the platform and SDK version it last saw. With `debug: true` the SDK prints each step, and **Real-Time Events** lists your events and purchases. For the full end-to-end test and the integration status checklist, see [Verify the integration](https://docs.influ.to/integrations/testing.md). To test attribution again on the same device, `clearAttribution()` forgets the stored attribution, code and user ID. It's for testing only — deleting and reinstalling the app gives a cleaner start. ## iOS notes - **Privacy manifest included.** The package ships `PrivacyInfo.xcprivacy`: no tracking, no tracking domains, and the data types it collects (not linked to identity). It doesn't use the advertising identifier, so no App Tracking Transparency prompt is needed. Details for your App Privacy label: [Privacy & App Store](https://docs.influ.to/advanced/privacy.md). - **StoreKit 2 only.** Store-direct reporting needs the signed transaction (JWS); never send the unsigned JSON representation. The SDK sends the JWS even when on-device verification fails, and InfluTo verifies it on the server. - **Sandbox vs Production.** TestFlight and sandbox purchases only verify when the app's Apple environment is set to Sandbox in **Settings → Validation**. - **RevenueCat order**: configure RevenueCat, then `initialize`, then `checkAttribution`. - **Local state** lives in the `com.influto.sdk` `UserDefaults` suite and survives app updates; deleting the app removes it. --- # Add InfluTo to an Android app > Add the InfluTo Android SDK from Maven Central, detect referred installs, accept typed codes, and connect RevenueCat or Google Play purchases. The InfluTo Android SDK adds creator attribution to a native Android app written in Kotlin. It tells you whether an install came from a creator's link or code, lets people type a creator's code, and gets each purchase to InfluTo — through RevenueCat, or straight from Google Play Billing. Plan for an afternoon; most of it goes into the purchase side. | | | |---|---| | Package | `to.influ:android-sdk`, plus optional `to.influ:android-sdk-ui` (Compose) and `to.influ:android-sdk-billing` (Play Billing capture) | | Latest version | 1.1.1 (all three artifacts) | | Registry | [Maven Central](https://central.sonatype.com/artifact/to.influ/android-sdk) | | Requires | minSdk 24, Java 17. Built with Kotlin 2.2, compileSdk 35 | | Dependencies | Core: kotlinx-coroutines only | | Source | [github.com/influto/influto-android](https://github.com/influto/influto-android) (MIT) | ## Install ```kotlin // build.gradle.kts dependencies { implementation("to.influ:android-sdk:1.1.1") // optional: Compose code field (InfluToReferralCodeInput) implementation("to.influ:android-sdk-ui:1.1.1") // optional: store-direct purchase auto-capture implementation("to.influ:android-sdk-billing:1.1.1") } ``` The artifacts are on Maven Central, so `mavenCentral()` must be in your repositories. Add only what you need — each add-on brings the core SDK with it: | Artifact | Add it when | What it brings | |---|---|---| | `android-sdk` | Always | The SDK; adds the `INTERNET` permission | | `android-sdk-ui` | You want the ready-made Compose code field | `InfluToReferralCodeInput` (Material 3) | | `android-sdk-billing` | Your app is store-direct (no RevenueCat) | Automatic purchase capture. Adds Google Play Billing Library 9.0.0 (`billing-ktx`) to your build and the `com.android.vending.BILLING` permission | The core and billing artifacts ship their own R8/ProGuard rules; there is nothing to add. ## Initialize ```kotlin import to.influ.sdk.InfluTo import to.influ.sdk.InfluToConfig // Call once at startup. initialize() is a suspend function — launch it from a // coroutine, or use the callback overload: InfluTo.initialize(context, config) { result -> } lifecycleScope.launch { InfluTo.initialize(applicationContext, InfluToConfig( apiKey = "YOUR_API_KEY", // RevenueCat apps: optional explicit setter (otherwise found by reflection) // revenueCatAttributeSetter = { attrs -> Purchases.sharedInstance.setAttributes(attrs) }, )) } ``` `initialize` is a `suspend` function. Call it once at startup and wait for it before calling anything else — if it hasn't succeeded, `checkAttribution` and `reportPurchase` throw `InfluToError.NotInitialized`. It throws when it fails (a wrong key, no network), so catch the error, log it, and carry on; the next launch tries again. A typical startup, in order: ```kotlin file="MyApp.kt" class MyApp : Application() { private val appScope = MainScope() override fun onCreate() { super.onCreate() // RevenueCat apps: configure it first Purchases.configure(PurchasesConfiguration.Builder(this, "goog_…").build()) appScope.launch { try { InfluTo.initialize(this@MyApp, InfluToConfig( apiKey = BuildConfig.INFLUTO_API_KEY, appVersion = BuildConfig.VERSION_NAME, )) } catch (e: Exception) { Log.w("InfluTo", "initialize failed", e) // retried on the next launch return@launch } val attribution = InfluTo.checkAttribution() InfluTo.identifyUser(Purchases.sharedInstance.appUserID) } } } ``` The API key starts with `it_` and is on **Setup & SDK**; one key works for development and production. Keep it out of committed source — for example in a `BuildConfig` field filled from a Gradle property kept in `local.properties` or your CI secrets. Not in a coroutine? Every network method also has a callback overload that takes `(Result) -> Unit`, for example `InfluTo.initialize(context, config) { result -> … }`. Callbacks run on a background thread, so switch to the main thread before touching UI. | Parameter | Type | Default | What it does | |---|---|---|---| | `apiKey` | `String` | required | The app's SDK key (`it_…`) | | `debug` | `Boolean` | `false` | Logs to Logcat under the `InfluTo` tag | | `appVersion` | `String?` | `null` (sent as `"unknown"`) | Your app version, sent with the startup call | | `autoCapture` | `Boolean` | `true` | Store-direct apps: start Play Billing capture when `android-sdk-billing` is on the classpath | | `revenueCatAttributeSetter` | `((Map) -> Unit)?` | `null` | RevenueCat apps: how the SDK sets subscriber attributes. If `null`, the SDK finds RevenueCat on its own | | `oneTimeProductIds` | `Set` | empty | Your one-time and consumable product IDs, so captured purchases are validated as one-time purchases | ## Detect referred users ```kotlin val attribution = InfluTo.checkAttribution() // suspend if (attribution.attributed) { // User came from a referral — show a special offer. Log.d("App", "Referred by ${attribution.referralCode}") } ``` `AttributionResult` has these properties: | Property | Meaning | |---|---| | `attributed` | `true` when the install came from a creator's link or code | | `referralCode` | The creator's code, uppercase | | `attributionMethod` | `"ip_device_match"` (link-click matching) or `"manual_entry"` (a code the user applied) | | `clickedAt` | When the link was clicked, or the code applied | | `message` | A short explanation, for logs | Attribution is decided once per install and stored on the device; later calls return the stored answer without a request. Only a failed request is retried, on the next call. A reinstall starts over. `checkAttribution` is fail-soft: on a network or server error it returns `attributed = false` and stores nothing. Link-click matching only counts taps from the last 72 hours — see [attribution](https://docs.influ.to/advanced/attribution.md). To show referred users a better offer, see [Reward referred users](https://docs.influ.to/guides/referral-offers.md). ## Identify the user Call `identifyUser` as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID: ```kotlin InfluTo.identifyUser(Purchases.sharedInstance.appUserID) ``` It matters for three things: InfluTo uses it to credit a RevenueCat purchase when the `influto_code` attribute didn't make it, `checkAccess` uses it for free-access codes, and `reportPurchase` sends it by default. Call it once per sign-in, ideally in the user's first session. ## Track events ```kotlin InfluTo.trackEvent( eventType = "paywall_viewed", appUserId = userId, properties = mapOf("placement" to "onboarding"), ) ``` Events show on **Real-Time Events** and help you read the funnel. They never create conversions or commission — only validated purchases do. Every call gets an event ID, so an accidental double call counts once. For `trial_started`, `subscription_purchased` and `subscription_renewed` the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence (an order ID, say) in `properties` when the event can repeat. For your own once-only events, pass `eventId`. ## Let users type a code ```kotlin // Validate + apply a code the user typed val result = InfluTo.applyCode("FITGURU30") // suspend if (result.valid && result.applied == true) { // Unlock the offer } // Or the Compose field (android-sdk-ui): InfluToReferralCodeInput(onApplied = { result -> … }) // Free-access codes (after identifyUser): setReferralCode(code).grantsAccess == true // → unlock premium, and gate premium on InfluTo.checkAccess().hasAccess. ``` `applyCode(code, appUserId)` validates the code and applies it if it's valid; the result's `applied` tells you whether it was applied. Codes are trimmed and uppercased for you. An invalid code comes back with `valid = false` and an `errorCode`: `INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED`, `NETWORK_ERROR` or `UNKNOWN`. If you build your own field, write your own copy: `message` and `influencer` can contain the creator's personal name. The ready-made field is the composable `InfluToReferralCodeInput` from `android-sdk-ui`. It validates as the user types and applies the code when they tap the button: ```kotlin InfluToReferralCodeInput( appUserId = userId, onApplied = { result -> /* the code is applied — unlock the offer */ }, ) ``` | Parameter | Default | Notes | |---|---|---| | `appUserId` | identified user | The user to bind the code to; free-access codes need one | | `autoPrefill` / `autoValidate` | `true` / `false` | Pre-fill the code from a link-attributed install, and validate it right away | | `showCampaignName` / `showReferrerName` | `false` / `false` | Show a creator's name only with their consent | | `title`, `placeholder`, `applyLabel`, `validMessage`, `invalidMessage` | — | Texts, for translation | | `onValidated`, `onApplied`, `modifier` | — | Callbacks and layout | More on the UX: [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). All parameters: [UI components](https://docs.influ.to/reference/ui.md). ## Purchases InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see [Choose a validation path](https://docs.influ.to/integrations.md). ### RevenueCat There's nothing to wire in code: when `checkAttribution` finds a referral, or a code is applied, the SDK finds the RevenueCat SDK and sets `influto_code` (the code) and `influto_referral` (the string `"true"`) on the subscriber. To be explicit, pass the setter yourself: ```kotlin revenueCatAttributeSetter = { attrs -> Purchases.sharedInstance.setAttributes(attrs) }, ``` Then connect the webhook — see [RevenueCat](https://docs.influ.to/integrations/revenuecat.md). Don't add `android-sdk-billing` to a RevenueCat app; it isn't needed. > [!IMPORTANT] > The SDK writes the attribute once, when the referral is found. If RevenueCat isn't configured at that moment, the write is skipped and not retried. Configure RevenueCat before `checkAttribution`, and call `identifyUser` with the RevenueCat app user ID as the fallback. ### Store-direct Add `android-sdk-billing`. When the app is set to store-direct in **Settings → Validation** and `autoCapture` is `true`, `initialize` starts an observer with its own Play Billing connection, which reports new purchases and sweeps existing ones each time the app starts. Each purchase is reported once, keyed on its purchase token; only purchases in the PURCHASED state are sent. If your app has its own `BillingClient` (most do), also hand each purchase to the module from your `PurchasesUpdatedListener` so it's reported the moment it happens. It shares the same de-duplication, so nothing is reported twice: ```kotlin override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList?) { if (result.responseCode != BillingClient.BillingResponseCode.OK || purchases == null) return scope.launch { for (purchase in purchases) { InfluToBilling.report(context, purchase) // subscriptions // one-time products: InfluToBilling.report(context, purchase, oneTime = true, price = 4.99, currency = "EUR") } } } ``` `InfluToBilling` also offers `startObservation(context, oneTimeProductIds)`, `stopObservation()` and `syncExistingPurchases(context)`. To report purchases yourself with the core SDK instead, set `autoCapture = false` and call `reportPurchase`: ```kotlin // Store-direct only (no RevenueCat). With the android-sdk-billing artifact on the // classpath, purchases are captured AUTOMATICALLY. To report manually: InfluTo.reportPurchase( purchaseToken = purchase.purchaseToken, // productId = "coins_100", // one-time / consumable products only ) ``` ```kotlin try { InfluTo.reportPurchase(purchaseToken = purchase.purchaseToken) } catch (e: InfluToError.Server) { if (e.retryable) { /* 503: exchange rate briefly unavailable — retry later */ } if (e.notConfigured) { /* 400: Android isn't set to store-direct yet */ } } ``` - One-time and consumable products need `productId`, plus `price` and `currency` from the product details. Without `productId` the purchase is checked as a subscription and fails; without a price it's recorded at $0. Auto-capture adds both for products listed in `oneTimeProductIds`. - `409` means a one-time purchase isn't in the PURCHASED state yet (for example, still pending). - Other errors: `InfluToError.NotInitialized`, `Transport` (no connection) and `Decoding`. Set-up, the Play service account and testing: [Store-direct](https://docs.influ.to/integrations/store-direct.md). ## Free-access codes Free-access codes give a user premium without a purchase. Redeem one with `setReferralCode(code, appUserId = userId)` after `identifyUser`, or pass the user ID — a code with no user is refused. The `SetCodeResult` tells you what happened: `grantsAccess`, `freeAccess`, `entitlement` and `expiresAt`. `applyCode` doesn't return these flags; after it, `result.campaign?.campaignType == "FREE_ACCESS"` tells you the code was a free-access code. Gate premium on your store entitlement or InfluTo's answer: ```kotlin val premium = entitlementActive || InfluTo.checkAccess().hasAccess ``` `checkAccess(appUserId)` uses the identified user when you don't pass one, caches a positive answer for 5 minutes (across launches too), never caches a negative one, and returns `hasAccess = false` on errors. See [Free access codes](https://docs.influ.to/platform/creator-access-codes.md). ## Verify After the first successful `initialize`, **Setup & SDK** shows "Integrated — SDK connected" with the platform and SDK version it last saw. With `debug = true` the SDK logs each step under the `InfluTo` tag, and **Real-Time Events** lists your events and purchases. For the full end-to-end test and the integration status checklist, see [Verify the integration](https://docs.influ.to/integrations/testing.md). To test attribution again on the same device, `clearAttribution()` forgets the stored attribution, code and user ID. It's for testing only — clearing the app's storage or reinstalling gives a cleaner start. ## Android notes - **Coroutines first.** Network methods are `suspend`; `getReferralCode()`, `getPrefilledCode()` and `clearAttribution()` are plain functions. Callback overloads deliver results on a background thread. - **Initialize once,** in `Application.onCreate` or with an app-wide scope — an activity's `onCreate` runs again on every configuration change. - **minSdk 24** and a Java 17 toolchain. - **Permissions**: the core adds `INTERNET`; `android-sdk-billing` adds `com.android.vending.BILLING`. For the Play Data safety form, see [Privacy & App Store](https://docs.influ.to/advanced/privacy.md). - **Play Billing version**: `android-sdk-billing` exposes Play Billing Library 9.0.0, which Gradle resolves together with your own Billing dependency. --- # Add InfluTo to a Flutter app > Add the influto package to a Flutter app, detect referred installs, accept typed codes, and connect RevenueCat or report store purchases with influto_iap. The `influto` package adds creator attribution to a Flutter app on iOS and Android. It tells you whether an install came from a creator's link or code, lets people type a creator's code, and gets each purchase to InfluTo — through RevenueCat, or straight from the App Store and Google Play with the companion package `influto_iap`. Plan for an afternoon; most of it goes into the purchase side. | | | |---|---| | Package | `influto`, plus the optional companion [`influto_iap`](https://pub.dev/packages/influto_iap) for automatic purchase capture | | Latest version | 1.1.1 | | Registry | [pub.dev](https://pub.dev/packages/influto) | | Requires | Flutter 3.22+, Dart 3.4+ | | Dependencies | `http` and `shared_preferences` (`influto_iap` adds `in_app_purchase`) | | Source | [github.com/influto/influto-flutter](https://github.com/influto/influto-flutter) (MIT) | ## Install ```bash flutter pub add influto # optional: automatic store-direct purchase capture flutter pub add influto_iap ``` Add `influto_iap` only if your app is store-direct (no RevenueCat) and sells through the `in_app_purchase` plugin. RevenueCat apps using `purchases_flutter` only need `influto`. ## Initialize ```dart import 'package:influto/influto.dart'; // Call once at startup await InfluTo.instance.initialize( InfluToConfig( apiKey: 'YOUR_API_KEY', // RevenueCat apps: let the SDK tag the subscriber // revenueCatHook: (attrs) => Purchases.setAttributes(attrs), ), ); ``` Call `initialize` once at startup and wait for it before calling anything else — if it hasn't succeeded, `checkAttribution` and `reportPurchase` throw an `InfluToException`. It throws when it fails (a wrong key, no network), so catch the error, log it, and carry on; the next launch tries again. Don't hold up your first frame for it. A typical startup, in order: ```dart file="main.dart" import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:influto/influto.dart'; import 'package:purchases_flutter/purchases_flutter.dart'; // only if you use RevenueCat Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Purchases.configure(PurchasesConfiguration('appl_…')); // RevenueCat apps: configure it first runApp(const MyApp()); unawaited(startInfluTo()); } Future startInfluTo() async { try { await InfluTo.instance.initialize(InfluToConfig( apiKey: const String.fromEnvironment('INFLUTO_API_KEY'), appVersion: '1.4.0', revenueCatHook: (attrs) => Purchases.setAttributes(attrs), )); } catch (e) { debugPrint('InfluTo initialize failed: $e'); // retried on the next launch return; } final attribution = await InfluTo.instance.checkAttribution(); await InfluTo.instance.identifyUser(await Purchases.appUserID); } ``` The API key starts with `it_` and is on **Setup & SDK**; one key works for development and production. Keep it out of committed source — for example pass it at build time with `--dart-define=INFLUTO_API_KEY=it_…` and read it with `String.fromEnvironment`. | Parameter | Type | Default | What it does | |---|---|---|---| | `apiKey` | `String` | required | The app's SDK key (`it_…`) | | `debug` | `bool` | `false` | Prints `[InfluTo]` lines with `debugPrint` | | `appVersion` | `String?` | `null` (sent as `'unknown'`) | Your app version, sent with the startup call | | `autoCapture` | `bool` | `true` | Store-direct apps: lets the `influto_iap` observer report purchases. Set `false` if you call `reportPurchase` yourself | | `revenueCatHook` | `Future Function(Map)?` | `null` | RevenueCat apps: how the SDK sets the subscriber attributes | | `httpClient` | `http.Client?` | `null` | A custom HTTP client, for tests | ## Detect referred users ```dart final attribution = await InfluTo.instance.checkAttribution(); if (attribution.attributed) { // User came from a referral — show a special offer. print('Referred by ${attribution.referralCode}'); } ``` `AttributionResult` has these fields: | Field | Meaning | |---|---| | `attributed` | `true` when the install came from a creator's link or code | | `referralCode` | The creator's code, uppercase | | `attributionMethod` | `'ip_device_match'` (link-click matching) or `'manual_entry'` (a code the user applied) | | `clickedAt` | When the link was clicked, or the code applied | | `message` | A short explanation, for logs | Attribution is decided once per install and stored on the device; later calls return the stored answer without a request. Only a failed request is retried, on the next call. A reinstall starts over. `checkAttribution` is fail-soft: on a network or server error it returns `attributed: false` and stores nothing. Link-click matching only counts taps from the last 72 hours — see [attribution](https://docs.influ.to/advanced/attribution.md). To show referred users a better offer, see [Reward referred users](https://docs.influ.to/guides/referral-offers.md). ## Identify the user Call `identifyUser` as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID: ```dart await InfluTo.instance.identifyUser(await Purchases.appUserID); ``` It matters for three things: InfluTo uses it to credit a RevenueCat purchase when the `influto_code` attribute didn't make it, `checkAccess` uses it for free-access codes, and `reportPurchase` sends it by default. Call it once per sign-in, ideally in the user's first session — not from `build`. ## Track events ```dart await InfluTo.instance.trackEvent(TrackEventOptions( eventType: 'paywall_viewed', appUserId: userId, properties: {'placement': 'onboarding'}, )); ``` Events show on **Real-Time Events** and help you read the funnel. They never create conversions or commission — only validated purchases do. Every call gets an event ID, so an accidental double call counts once. For `trial_started`, `subscription_purchased` and `subscription_renewed` the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence (a transaction ID, say) in `properties` when the event can repeat. For your own once-only events, pass `eventId`. ## Let users type a code ```dart // Validate + apply a code the user typed final result = await InfluTo.instance.applyCode('FITGURU30'); if (result.valid && result.applied == true) { // Unlock the offer } // Or the ready-made widget: ReferralCodeInput(onApplied: (result) { … }) // Free-access codes (after identifyUser): (await setReferralCode(code)).grantsAccess // == true → unlock premium, and gate premium on (await checkAccess()).hasAccess. ``` `applyCode(code, appUserId: …)` validates the code and applies it if it's valid; the result's `applied` tells you whether it was applied. Codes are trimmed and uppercased for you. An invalid code comes back with `valid: false` and an `errorCode`: `INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED` or `NETWORK_ERROR`. If you build your own field, write your own copy: `message` and `influencer` can contain the creator's personal name. The ready-made field is the `ReferralCodeInput` widget (Material). It validates as the user types and applies the code when they tap the button: ```dart ReferralCodeInput( appUserId: userId, onApplied: (result) { /* the code is applied — unlock the offer */ }, ) ``` | Parameter | Default | Notes | |---|---|---| | `appUserId` | identified user | The user to bind the code to; free-access codes need one | | `autoPrefill` / `autoValidate` | `true` / `false` | Pre-fill the code from a link-attributed install, and validate it right away | | `showCampaignName` / `showReferrerName` | `false` / `false` | Show a creator's name only with their consent | | `title`, `placeholder`, `applyLabel`, `validMessage`, `invalidMessage` | — | Texts, for translation | | `onValidated`, `onApplied` | — | Called after each validation, and after the code is applied | More on the UX: [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). All parameters: [UI components](https://docs.influ.to/reference/ui.md). ## Purchases InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see [Choose a validation path](https://docs.influ.to/integrations.md). ### RevenueCat Pass a `revenueCatHook`, as in the startup example above. When `checkAttribution` finds a referral, or a code is applied, the SDK calls it with `influto_code` (the code) and `influto_referral` (the string `"true"`). Then connect the webhook — see [RevenueCat](https://docs.influ.to/integrations/revenuecat.md). > [!IMPORTANT] > Without the hook, nothing sets the attribute in Flutter. The SDK calls it once, when the referral is found, so configure RevenueCat before `checkAttribution`, and call `identifyUser` with the RevenueCat app user ID as the fallback. ### Store-direct with influto_iap Create one `InfluToPurchaseObserver` and enable it after `initialize` has finished: ```dart import 'package:influto_iap/influto_iap.dart'; final observer = InfluToPurchaseObserver( oneTimeProductIds: {'coins_100', 'remove_ads'}, // Android one-time products; omit if you only sell subscriptions ); final sync = await observer.enable(); // listens for new purchases + back-fills existing ones ``` The observer only runs when the app is set to store-direct in **Settings → Validation** and `autoCapture` is `true`; otherwise `enable()` does nothing, so the same build is safe on RevenueCat apps. Each purchase is reported once. `enable()` returns a `PurchaseSyncResult` with `fetched`, `sent` and `failed` counts; `syncExisting()`, `disable()` and `dispose()` are there for manual control. What the observer does in your app: - It completes pending purchases (`completePurchase`), so the store doesn't redeliver or refund them. Deliver content from your own `purchaseStream` listener as usual. - The back-fill calls `restorePurchases()`, so your own listener also sees `restored` purchases at startup. Call `enable(backSync: false)` if you don't want that. - On iOS it switches `in_app_purchase` to StoreKit 2, which store-direct needs. To report purchases yourself instead, set `autoCapture: false`, skip the observer, and call `reportPurchase`: ```dart // Store-direct only (no RevenueCat). For automatic capture add influto_iap and // call InfluToPurchaseObserver().enable() after initialize(). Or report manually: await InfluTo.instance.reportPurchase( platform: 'ios', // or 'android' signedTransaction: jws, // iOS: StoreKit 2 JWS // purchaseToken: token, // Android: Play Billing purchaseToken ); ``` - `platform` is required: `'ios'` or `'android'`. On iOS pass the StoreKit 2 signed transaction, `details.verificationData.serverVerificationData` — with `in_app_purchase` on StoreKit 2 (`InAppPurchaseStoreKitPlatform.enableStoreKit2()` makes sure of it); on Android pass `(details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseToken`. - It throws an `InfluToException`. `retryable` is true for `503` (the exchange rate was briefly unavailable) — retry later; `notConfigured` is true for `400` (the platform isn't set to store-direct yet). - Android one-time and consumable products need `productId`, plus `price` and `currency` from the product details; without a price the purchase is recorded at $0. A `409` means the purchase isn't in the PURCHASED state yet. Set-up, credentials and testing: [Store-direct](https://docs.influ.to/integrations/store-direct.md). ## Free-access codes Free-access codes give a user premium without a purchase. Redeem one with `setReferralCode(code, appUserId: userId)` after `identifyUser`, or pass the user ID — a code with no user is refused. The `SetCodeResult` tells you what happened: `grantsAccess`, `freeAccess`, `entitlement` and `expiresAt`. `applyCode` doesn't return these flags; after it, `result.campaign?.campaignType == 'FREE_ACCESS'` tells you the code was a free-access code. Gate premium on your store entitlement or InfluTo's answer: ```dart final premium = entitlementActive || (await InfluTo.instance.checkAccess()).hasAccess; ``` `checkAccess(appUserId: …)` uses the identified user when you don't pass one, caches a positive answer for 5 minutes (across launches too), never caches a negative one, and returns `hasAccess: false` on errors. See [Free access codes](https://docs.influ.to/platform/creator-access-codes.md). ## Verify After the first successful `initialize`, **Setup & SDK** shows "Integrated — SDK connected" with the platform and SDK version it last saw. With `debug: true` the SDK prints each step, and **Real-Time Events** lists your events and purchases. For the full end-to-end test and the integration status checklist, see [Verify the integration](https://docs.influ.to/integrations/testing.md). To test attribution again on the same device, `clearAttribution()` forgets the stored attribution, code and user ID. It's for testing only — a reinstall gives a cleaner start. ## Flutter notes - **One instance.** Everything goes through `InfluTo.instance`; there are no static calls. - **Order matters**: configure RevenueCat, then `initialize`, then `checkAttribution`; enable the `influto_iap` observer only after `initialize` has finished, or it stays off. - **`const` configs**: `InfluToConfig` has a `const` constructor, but a closure such as `(attrs) => …` isn't a constant — drop `const` when you pass a `revenueCatHook`. - **No automatic RevenueCat tagging.** Unlike React Native, Flutter needs the `revenueCatHook`. - **Android one-time products** go in the observer's `oneTimeProductIds`, not in `InfluToConfig`. --- # Add InfluTo to a Capacitor, Ionic, Cordova or web app > Use @influto/web in Capacitor, Ionic, Cordova, PWAs and browsers — capture referral links, accept typed codes, and connect RevenueCat or store purchases. `@influto/web` adds creator attribution to any app whose code runs in a WebView or a browser: Capacitor (including Ionic React, Angular and Vue), Apache Cordova, installed PWAs, Trusted Web Activities and plain web apps. It follows the same contract as the native SDKs, and adds web-only ways to pick up a referral exactly: the launch URL, deep links, and the Google Play Install Referrer. Plan for an afternoon; most of it goes into the purchase side. | | | |---|---| | Package | `@influto/web`, with subpaths `/capacitor`, `/cordova`, `/pwa`, `/revenuecat` and `/react` | | Latest version | 1.0.1 | | Registry | [npm](https://www.npmjs.com/package/@influto/web) | | Runtimes | Capacitor / Ionic, Cordova, PWAs and TWAs, browsers — any ES2019 browser or WebView | | Builds | ESM, CommonJS, and a script-tag build | | Dependencies | None. React 17+ only if you use `@influto/web/react` | ## Install ```bash npm install @influto/web # Capacitor / Ionic apps (device info, storage, deep links): npm install @capacitor/device @capacitor/preferences @capacitor/app ``` The Capacitor plugins are optional but recommended: `@capacitor/device` for device details, `@capacitor/preferences` for storage that survives WebView resets, `@capacitor/app` for deep links. Run `npx cap sync` after adding them. Missing plugins fall back to browser defaults. No bundler? Load `https://cdn.jsdelivr.net/npm/@influto/web@1/dist/index.global.js` with a `