# SDK reference

> Every public method of the five InfluTo SDKs — exact signatures per platform, result fields, what throws, what fails soft, caching and local storage.

This page lists every public method of the InfluTo SDKs, with the exact signature on each platform. It covers React Native 1.6.1, iOS 1.1.1, Android 1.1.1, Flutter 1.1.1 and web 1.0.1. For setup steps, start with your platform's [install guide](https://docs.influ.to/sdk.md); for the HTTP calls behind each method, see the [HTTP API](https://docs.influ.to/reference/http-api.md).

## At a glance

| Method | What it does | Throws? | Notes |
|---|---|---|---|
| [`initialize`](#initialize) | Connects the SDK with your app's key | Yes | Call once at startup; every other method needs it |
| [`checkAttribution`](#checkattribution) | Decides once per install whether a creator referred it | Only if called before `initialize` succeeded (not on iOS) | Result is stored; network errors return "not attributed" |
| [`identifyUser`](#identifyuser) | Links your user ID to this install | No | Lets purchases without the RevenueCat attribute still attribute |
| [`trackEvent`](#trackevent) | Records a custom funnel event | No | Never creates a conversion or commission |
| [`getActiveCampaigns`](#getactivecampaigns) | Lists the app's active public campaigns | No | `[]` on error |
| [`getReferralCode`](#getreferralcode) | Reads the stored code | No | Local read |
| [`getPrefilledCode`](#getprefilledcode) | The stored code, only when this install is attributed | No | Local read; use it to prefill a code field |
| [`validateCode`](#validatecode) | Checks a typed code without applying it | No | `NETWORK_ERROR` on failure |
| [`setReferralCode`](#setreferralcode) | Applies a code: stores it, tags RevenueCat, records it | No | Returns the free-access fields |
| [`applyCode`](#applycode) | `validateCode`, then `setReferralCode` if valid | No | Adds `applied`; no free-access fields |
| [`checkAccess`](#checkaccess) | Whether InfluTo grants this user free access | No | Positive result cached for 5 minutes |
| [`reportPurchase`](#reportpurchase) | Sends a store-signed purchase for verification (store-direct) | Yes | `503` means retry |
| [Automatic purchase capture](#automatic-purchase-capture) | Reports store purchases for you on store-direct platforms | Varies | Names differ per platform |
| [`clearAttribution`](#clearattribution) | Forgets the local attribution | No | Local only; for testing |

## Conventions

- **Fail-soft.** Only `initialize` and `reportPurchase` throw on network or server errors. Everything else returns a safe value (`attributed: false`, `[]`, `valid: false`, `success: false`, `hasAccess: false`) and logs the error when `debug` is on.
- **Before `initialize` succeeds**, methods return their fail-soft values without a request. `checkAttribution` and `reportPurchase` throw instead (on iOS, `checkAttribution` returns `attributed: false` with the message `SDK not initialized`). On React Native, `enableAutoPurchaseCapture` and `syncExistingPurchases` also throw.
- **Codes** are trimmed and uppercased by the SDK before they're sent. The server also ignores any character outside `A–Z 0–9 _ -` when it looks a code up, but the SDK stores — and writes to RevenueCat — the code as you passed it, so strip other characters (spaces, invisible characters from copy-paste) in your own input field. See [code rules](https://docs.influ.to/reference/limits-and-errors.md#code-format).
- **Async style.** React Native and web return Promises; iOS methods are `async` statics on `InfluTo`; Flutter uses `InfluTo.instance` and Futures; Android methods are `suspend` functions on `object InfluTo`, and every network method also has an overload whose last parameter is a `(Result<T>) -> Unit` callback, for Java or non-coroutine code.
- **Field names.** React Native and web keep the server's snake_case in validation and purchase results (`campaign_type`, `error_code`, `event_type`); iOS, Android and Flutter use camelCase (`campaignType`, `errorCode`, `eventType`). See [Result types](#result-types).

## initialize

Connects the SDK with your app's key (`POST /sdk/init`). Call it once when the app starts, before anything else.

<!-- tabs -->
```tsx title="React Native / Expo"
// initialize(config: InfluToConfig): Promise<void>
import InfluTo from '@influto/react-native-sdk';

await InfluTo.initialize({ apiKey: 'it_…', debug: __DEV__, appVersion: '1.4.0' });
```
```swift title="iOS (Swift)"
// static func initialize(_ config: InfluToConfig) async throws
import InfluTo

try await InfluTo.initialize(InfluToConfig(apiKey: "it_…", debug: true, appVersion: "1.4.0"))
```
```kotlin title="Android (Kotlin)"
// suspend fun initialize(context: Context, config: InfluToConfig)
// fun initialize(context: Context, config: InfluToConfig, callback: (Result<Unit>) -> Unit)
import to.influ.sdk.InfluTo
import to.influ.sdk.InfluToConfig

lifecycleScope.launch {
    InfluTo.initialize(applicationContext, InfluToConfig(apiKey = "it_…", appVersion = BuildConfig.VERSION_NAME))
}
```
```dart title="Flutter (Dart)"
// Future<void> initialize(InfluToConfig config)
import 'package:influto/influto.dart';

await InfluTo.instance.initialize(const InfluToConfig(apiKey: 'it_…', appVersion: '1.4.0'));
```
```ts title="Web / Capacitor / PWA"
// initialize(config: InfluToConfig): Promise<InitResult>
import InfluTo from '@influto/web';
import { capacitorAdapters } from '@influto/web/capacitor';

const init = await InfluTo.initialize({ apiKey: 'it_…', appVersion: '1.4.0', ...(await capacitorAdapters()) });
```
<!-- /tabs -->

**Configuration**

| Field | Platforms | Default | What it does |
|---|---|---|---|
| `apiKey` | All | required | Your app's SDK key (`it_…`) from **Setup & SDK**. One key covers sandbox and production. |
| `debug` | All | `false` | Logs calls and errors. |
| `appVersion` | All | `"unknown"` | Your app's version, sent with `/sdk/init`. |
| `autoCapture` | All | `true` | Reports store purchases automatically on store-direct platforms. On Flutter it's read by the `influto_iap` observer. See [Automatic purchase capture](#automatic-purchase-capture). |
| `oneTimeProductIds` | React Native, Android, web | none | Your Android one-time and consumable product IDs, so captured purchases are validated as one-time rather than subscriptions. Array on React Native and web, `Set<String>` on Android. |
| `revenueCatAttributeSetter` | iOS, Android | `nil` / `null` | Function the SDK calls with the RevenueCat attributes. Android falls back to finding the RevenueCat SDK by reflection when it's not set. |
| `revenueCatHook` | Flutter | `null` | `Future<void> Function(Map<String, String>)`, same purpose. |
| `revenueCat` | Web | none | A function or an object with `setAttributes`; helpers in `@influto/web/revenuecat`. |
| `httpClient` | Flutter | default client | Injectable `http.Client` (tests, custom transport). |
| `runtime`, `storage`, `device`, `http` | Web | detected | Runtime and storage adapters; `capacitorAdapters()` and `cordovaAdapters()` fill them in. |
| `captureLaunchCode` | Web | `true` | Reads `influto_code` / `influto_click` from the page URL during `initialize`. |
| `cleanUrl` | Web | `true` | Removes the `influto_*` parameters from the address bar after reading them. |
| `purchaseSource` | Web | none | Store purchase source for automatic capture. |
| `apiURL` (iOS), `apiUrl` | iOS, Android, Flutter | `https://influ.to/api` | Base URL override for testing. |

React Native has no RevenueCat option: if `react-native-purchases` is installed, the SDK finds it and sets the attributes itself.

**Behavior**

- Throws on a bad key, a network failure or a server error. Catch it and continue; the SDK stays uninitialized for this launch (see [Conventions](#conventions)) and you can try again on the next one.
- If the response says the platform is store-direct (`store_direct: true`) and `autoCapture` isn't `false`, it starts automatic purchase capture.
- Web only: reads a referral code from the launch URL before the request, so a deep-linked referral survives a failed init, and returns the `/sdk/init` response as `InitResult` (`app_id`, `app_name`, `attribution_window_hours`, `campaigns`, `store_direct`, `initialized`).

## checkAttribution

Decides whether this install came from a creator. The first call sends `POST /sdk/track-install`; the result is stored and every later call returns it without a request.

<!-- tabs -->
```tsx title="React Native / Expo"
// checkAttribution(): Promise<AttributionResult>
const attribution = await InfluTo.checkAttribution();
if (attribution.attributed) console.log(attribution.referralCode);
```
```swift title="iOS (Swift)"
// static func checkAttribution() async -> AttributionResult
let attribution = await InfluTo.checkAttribution()
if attribution.attributed, let code = attribution.referralCode { print(code) }
```
```kotlin title="Android (Kotlin)"
// suspend fun checkAttribution(): AttributionResult
val attribution = InfluTo.checkAttribution()
if (attribution.attributed) Log.d("Referral", attribution.referralCode ?: "")
```
```dart title="Flutter (Dart)"
// Future<AttributionResult> checkAttribution()
final attribution = await InfluTo.instance.checkAttribution();
if (attribution.attributed) debugPrint(attribution.referralCode);
```
```ts title="Web / Capacitor / PWA"
// checkAttribution(): Promise<AttributionResult>
const attribution = await InfluTo.checkAttribution();
if (attribution.attributed) console.log(attribution.referralCode);
```
<!-- /tabs -->

Returns an [AttributionResult](#attributionresult).

- **Decided once per install.** Both outcomes, attributed and organic, are stored under `@influto/attribution`. A failed request returns `{ attributed: false, message: "Error checking attribution" }` and stores nothing, so the next launch asks again.
- **When attributed**, the code is stored under `@influto/influto_code` and the SDK sets the RevenueCat attributes `influto_code` = the code and `influto_referral` = `"true"` (a string). That happens automatically on React Native (with `react-native-purchases` installed) and Android; iOS, Flutter and web need the setter, hook or binding from [`initialize`](#initialize).
- **`attributionMethod`** is `link_param` (a link carried the code into the app), `ip_device_match` (link-click matching) or `manual_entry` (a code applied with `setReferralCode`). See [attribution](https://docs.influ.to/advanced/attribution.md).
- **Web:** a code captured from the launch URL or the Play Install Referrer is sent along and claimed exactly (`link_param`), and it replaces a stored organic result. Concurrent calls share one request.
- Throws before `initialize` succeeded, except on iOS (see [Conventions](#conventions)).

## identifyUser

Tells InfluTo who the user is — typically the RevenueCat app user ID, or your own user ID (`POST /sdk/identify`).

<!-- tabs -->
```tsx title="React Native / Expo"
// identifyUser(appUserId: string, properties?: Record<string, any>): Promise<void>
await InfluTo.identifyUser(userId, { plan: 'free' });
```
```swift title="iOS (Swift)"
// static func identifyUser(_ appUserId: String, properties: [String: AnyCodable]? = nil) async
await InfluTo.identifyUser(userID, properties: ["plan": AnyCodable("free")])
```
```kotlin title="Android (Kotlin)"
// suspend fun identifyUser(appUserId: String, properties: Map<String, Any?>? = null)
InfluTo.identifyUser(userId, mapOf("plan" to "free"))
```
```dart title="Flutter (Dart)"
// Future<void> identifyUser(String appUserId, {Map<String, dynamic>? properties})
await InfluTo.instance.identifyUser(userId, properties: {'plan': 'free'});
```
```ts title="Web / Capacitor / PWA"
// identifyUser(appUserId: string, properties?: Record<string, any>): Promise<void>
await InfluTo.identifyUser(userId, { plan: 'free' });
```
<!-- /tabs -->

- Stores the ID under `@influto/app_user_id` (before the request, so it's kept even if the request fails). `checkAccess` and `reportPurchase` use it when you don't pass a user ID.
- When the server can match this install's attributed click, it attaches the ID to it (the first identify wins). If a RevenueCat event later arrives without the `influto_code` attribute, InfluTo looks for an attributed click with the purchaser's app user ID — so call it early in the first session, as soon as you know the user.
- Call it once per launch, not on every render: identical calls (same key and user) within 60 seconds are answered from memory, and the endpoint is rate limited to 2 requests per second per IP ([limits](https://docs.influ.to/reference/limits-and-errors.md#rate-limits)).
- Fail-soft; does nothing before `initialize`.

## trackEvent

Records a custom event for the funnel on your dashboard (`POST /sdk/event`).

<!-- tabs -->
```tsx title="React Native / Expo"
// trackEvent(options: TrackEventOptions): Promise<void>
// TrackEventOptions { eventType: string; appUserId: string; properties?: Record<string, any>; referralCode?: string; eventId?: string }
await InfluTo.trackEvent({ eventType: 'paywall_viewed', appUserId: userId, properties: { placement: 'onboarding' } });
```
```swift title="iOS (Swift)"
// static func trackEvent(_ options: TrackEventOptions) async
// TrackEventOptions(eventType:appUserId:properties:referralCode:eventId:)
await InfluTo.trackEvent(TrackEventOptions(eventType: "paywall_viewed", appUserId: userID,
                                           properties: ["placement": AnyCodable("onboarding")]))
```
```kotlin title="Android (Kotlin)"
// suspend fun trackEvent(eventType: String, appUserId: String, properties: Map<String, Any?>? = null,
//                        referralCode: String? = null, eventId: String? = null)
InfluTo.trackEvent(eventType = "paywall_viewed", appUserId = userId, properties = mapOf("placement" to "onboarding"))
```
```dart title="Flutter (Dart)"
// Future<void> trackEvent(TrackEventOptions options)
// TrackEventOptions({required eventType, required appUserId, properties, referralCode, eventId})
await InfluTo.instance.trackEvent(const TrackEventOptions(
    eventType: 'paywall_viewed', appUserId: 'user_123', properties: {'placement': 'onboarding'}));
```
```ts title="Web / Capacitor / PWA"
// trackEvent(options: TrackEventOptions): Promise<void>
await InfluTo.trackEvent({ eventType: 'paywall_viewed', appUserId: userId, properties: { placement: 'onboarding' } });
```
<!-- /tabs -->

- **Never creates a conversion or a commission.** Money comes only from verified purchases (RevenueCat or store-direct).
- **Duplicates are dropped.** Without an `eventId`, the SDK generates one: for `trial_started`, `subscription_purchased` and `subscription_renewed` it's derived from the event type, user and properties, so these count once per user and property set even across launches; other events get a random UUID. Pass your own `eventId` to control this. The server also drops an identical event without an ID sent within 60 seconds.
- Fail-soft; does nothing before `initialize`.

## getActiveCampaigns

Lists the app's active **public** campaigns (`GET /sdk/campaigns`), for showing offers in the app.

<!-- tabs -->
```tsx title="React Native / Expo"
// getActiveCampaigns(): Promise<Campaign[]>
const campaigns = await InfluTo.getActiveCampaigns();
```
```swift title="iOS (Swift)"
// static func getActiveCampaigns() async -> [Campaign]
let campaigns = await InfluTo.getActiveCampaigns()
```
```kotlin title="Android (Kotlin)"
// suspend fun getActiveCampaigns(): List<Campaign>
val campaigns = InfluTo.getActiveCampaigns()
```
```dart title="Flutter (Dart)"
// Future<List<Campaign>> getActiveCampaigns()
final campaigns = await InfluTo.instance.getActiveCampaigns();
```
```ts title="Web / Capacitor / PWA"
// getActiveCampaigns(): Promise<Campaign[]>
const campaigns = await InfluTo.getActiveCampaigns();
```
<!-- /tabs -->

Each [Campaign](#campaign) has `id`, `name`, `description` and the creator commission percentage. Private and invite-only campaigns aren't included. Returns an empty list before `initialize` or on any error.

## getReferralCode

Returns the stored code — from attribution or from a code the user applied — or null. No request.

<!-- tabs -->
```tsx title="React Native / Expo"
// getReferralCode(): Promise<string | null>
const code = await InfluTo.getReferralCode();
```
```swift title="iOS (Swift)"
// static func getReferralCode() async -> String?
let code = await InfluTo.getReferralCode()
```
```kotlin title="Android (Kotlin)"
// fun getReferralCode(): String?   (not suspend)
val code = InfluTo.getReferralCode()
```
```dart title="Flutter (Dart)"
// Future<String?> getReferralCode()
final code = await InfluTo.instance.getReferralCode();
```
```ts title="Web / Capacitor / PWA"
// getReferralCode(): Promise<string | null>
const code = await InfluTo.getReferralCode();
```
<!-- /tabs -->

Reads `@influto/influto_code`. This is the code `reportPurchase` sends by default.

## getPrefilledCode

Returns the stored code only when this install is attributed — use it to prefill a code field. No request.

<!-- tabs -->
```tsx title="React Native / Expo"
// getPrefilledCode(): Promise<string | null>
const prefill = await InfluTo.getPrefilledCode();
```
```swift title="iOS (Swift)"
// static func getPrefilledCode() async -> String?
let prefill = await InfluTo.getPrefilledCode()
```
```kotlin title="Android (Kotlin)"
// fun getPrefilledCode(): String?   (not suspend)
val prefill = InfluTo.getPrefilledCode()
```
```dart title="Flutter (Dart)"
// Future<String?> getPrefilledCode()
final prefill = await InfluTo.instance.getPrefilledCode();
```
```ts title="Web / Capacitor / PWA"
// getPrefilledCode(): Promise<string | null>
const prefill = await InfluTo.getPrefilledCode();
```
<!-- /tabs -->

Reads the stored attribution: the code when `attributed` is true (from a link or an applied code), otherwise null.

## validateCode

Checks a code the user typed, without applying it (`POST /sdk/validate-code`). Matches creator codes and free-access codes.

<!-- tabs -->
```tsx title="React Native / Expo"
// validateCode(code: string): Promise<CodeValidationResult>
const check = await InfluTo.validateCode(input);
if (!check.valid) showError(check.error_code);
```
```swift title="iOS (Swift)"
// static func validateCode(_ code: String) async -> CodeValidationResult
let check = await InfluTo.validateCode(input)
if !check.valid { print(check.errorCode ?? "") }
```
```kotlin title="Android (Kotlin)"
// suspend fun validateCode(code: String): CodeValidationResult
val check = InfluTo.validateCode(input)
if (!check.valid) Log.d("Code", check.errorCode?.name ?: "")
```
```dart title="Flutter (Dart)"
// Future<CodeValidationResult> validateCode(String code)
final check = await InfluTo.instance.validateCode(input);
if (!check.valid) debugPrint(check.errorCode);
```
```ts title="Web / Capacitor / PWA"
// validateCode(code: string): Promise<CodeValidationResult>
const check = await InfluTo.validateCode(input);
if (!check.valid) showError(check.error_code);
```
<!-- /tabs -->

Returns a [CodeValidationResult](#codevalidationresult).

- Nothing is stored and RevenueCat isn't touched.
- A free-access code comes back valid with `campaign_type` `FREE_ACCESS` and the campaign name `Free Access`.
- For creator codes, `influencer.name` and the `message` text ("Valid code! Referred by …") contain the creator's personal name. Don't show them unless your creators agreed.
- Fail-soft: before `initialize` or on a network error, returns `{ valid: false, error_code: "NETWORK_ERROR" }`.

## setReferralCode

Applies a code: stores it locally as a `manual_entry` attribution, sets the RevenueCat attributes, and records it with InfluTo (`POST /sdk/set-referral-code`). For free-access codes, this is the call that grants access.

<!-- tabs -->
```tsx title="React Native / Expo"
// setReferralCode(code: string, appUserId?: string): Promise<SetCodeResult>
const result = await InfluTo.setReferralCode(code, userId);
if (result.grantsAccess) unlockPremium();
```
```swift title="iOS (Swift)"
// static func setReferralCode(_ code: String, appUserId: String? = nil) async -> SetCodeResult
let result = await InfluTo.setReferralCode(code, appUserId: userID)
if result.grantsAccess == true { unlockPremium() }
```
```kotlin title="Android (Kotlin)"
// suspend fun setReferralCode(code: String, appUserId: String? = null): SetCodeResult
val result = InfluTo.setReferralCode(code, userId)
if (result.grantsAccess == true) unlockPremium()
```
```dart title="Flutter (Dart)"
// Future<SetCodeResult> setReferralCode(String code, {String? appUserId})
final result = await InfluTo.instance.setReferralCode(code, appUserId: userId);
if (result.grantsAccess == true) unlockPremium();
```
```ts title="Web / Capacitor / PWA"
// setReferralCode(code: string, appUserId?: string): Promise<SetCodeResult>
const result = await InfluTo.setReferralCode(code, userId);
if (result.grantsAccess) unlockPremium();
```
<!-- /tabs -->

Returns a [SetCodeResult](#setcoderesult).

- **Only an accepted code is kept.** When the server answers `success: true`, the SDK stores the code (as the server matched it, look-alike characters corrected), records the referral and writes the RevenueCat attributes. A rejected code leaves nothing behind. Older versions stored the code before the answer (before React Native 1.6.1, iOS/Android/Flutter 1.1.1, web 1.0.1). For codes users type, [`applyCode`](#applycode) validates first and returns readable error codes.
- **Free-access codes need a user ID.** `appUserId` defaults to the user from `identifyUser` (React Native 1.6.1 and iOS/Android/Flutter 1.1.1 or later; the web SDK always did). With no user at all the server answers `success: false` ("Could not identify your account"). iOS, Android and Flutter also store a passed `appUserId` as the identified user.
- If this creator's link already attributed an install from the same IP address within the attribution window, the server answers `success: true` with the message "Code already attributed from link click" and records nothing new.
- An unknown or inactive code is a `404` from the server, which the SDK returns as `success: false`.
- Gate premium on [`checkAccess`](#checkaccess) (or the store entitlement); `grantsAccess` is for unlocking immediately.

## applyCode

The usual call for a typed code: runs `validateCode`, and if the code is valid, `setReferralCode`.

<!-- tabs -->
```tsx title="React Native / Expo"
// applyCode(code: string, appUserId?: string): Promise<CodeValidationResult & { applied?: boolean }>
const result = await InfluTo.applyCode(code, userId);
if (result.applied && result.campaign?.campaign_type === 'FREE_ACCESS') {
  const { hasAccess } = await InfluTo.checkAccess(userId);
  if (hasAccess) unlockPremium();
}
```
```swift title="iOS (Swift)"
// static func applyCode(_ code: String, appUserId: String? = nil) async -> CodeValidationResult
let result = await InfluTo.applyCode(code, appUserId: userID)
if result.applied == true, result.campaign?.campaignType == "FREE_ACCESS" {
    let access = await InfluTo.checkAccess(appUserId: userID)
    if access.hasAccess { unlockPremium() }
}
```
```kotlin title="Android (Kotlin)"
// suspend fun applyCode(code: String, appUserId: String? = null): CodeValidationResult
val result = InfluTo.applyCode(code, userId)
if (result.applied == true && result.campaign?.campaignType == "FREE_ACCESS") {
    if (InfluTo.checkAccess(userId).hasAccess) unlockPremium()
}
```
```dart title="Flutter (Dart)"
// Future<CodeValidationResult> applyCode(String code, {String? appUserId})
final result = await InfluTo.instance.applyCode(code, appUserId: userId);
if (result.applied == true && result.campaign?.campaignType == 'FREE_ACCESS') {
  final access = await InfluTo.instance.checkAccess(appUserId: userId);
  if (access.hasAccess) unlockPremium();
}
```
```ts title="Web / Capacitor / PWA"
// applyCode(code: string, appUserId?: string): Promise<CodeValidationResult & { applied?: boolean }>
const result = await InfluTo.applyCode(code, userId);
if (result.applied && result.campaign?.campaign_type === 'FREE_ACCESS') {
  const { hasAccess } = await InfluTo.checkAccess(userId);
  if (hasAccess) unlockPremium();
}
```
<!-- /tabs -->

- Returns the validation result plus `applied`: `true` when `setReferralCode` succeeded, `false` when the code was invalid or applying it failed.
- It does **not** return the free-access fields (`freeAccess`, `grantsAccess`, `entitlement`, `expiresAt`). To react to a free-access code, check `campaign_type` / `campaignType` for `FREE_ACCESS` and then call `checkAccess`, as above — or call `validateCode` and `setReferralCode` yourself and read `grantsAccess`.

## checkAccess

Asks InfluTo whether this user currently has free access from a free-access code (`GET /sdk/access`). Works the same for RevenueCat and store-direct apps.

<!-- tabs -->
```tsx title="React Native / Expo"
// checkAccess(appUserId?: string): Promise<AccessResult>
const premium = entitlementActive || (await InfluTo.checkAccess(userId)).hasAccess;
```
```swift title="iOS (Swift)"
// static func checkAccess(appUserId: String? = nil) async -> AccessResult
let access = await InfluTo.checkAccess(appUserId: userID)
let premium = entitlementActive || access.hasAccess
```
```kotlin title="Android (Kotlin)"
// suspend fun checkAccess(appUserId: String? = null): AccessResult
val premium = entitlementActive || InfluTo.checkAccess(userId).hasAccess
```
```dart title="Flutter (Dart)"
// Future<AccessResult> checkAccess({String? appUserId})
final premium = entitlementActive || (await InfluTo.instance.checkAccess(appUserId: userId)).hasAccess;
```
```ts title="Web / Capacitor / PWA"
// checkAccess(appUserId?: string): Promise<AccessResult>
const premium = entitlementActive || (await InfluTo.checkAccess(userId)).hasAccess;
```
<!-- /tabs -->

Returns an [AccessResult](#accessresult).

- Uses `appUserId`, or the ID stored by `identifyUser` / `setReferralCode`. With no user ID it returns `hasAccess: false` without a request.
- **Caching:** a positive result is cached for 5 minutes under `@influto/access` (iOS, Android and Flutter also keep it in memory). A negative result is never cached, so a code redeemed a moment ago shows up on the next call.
- The server decides: seat limits and expiry are enforced there, and blocking or expiring a code removes access for everyone who redeemed it once their cached result expires.
- Recommended premium gate: your store or RevenueCat entitlement **or** `checkAccess().hasAccess`.
- Fail-soft: any error returns `hasAccess: false`. `clearAttribution` doesn't clear it.

## reportPurchase

Store-direct only. Sends the store's signed proof of a purchase so InfluTo can verify it with Apple or Google and record the commission (`POST /sdk/purchase`). If automatic capture is on, you don't call this — use one reporting path, not both.

<!-- tabs -->
```tsx title="React Native / Expo"
// reportPurchase(options: ReportPurchaseOptions): Promise<PurchaseResult>
// { platform?, signedTransaction?, purchaseToken?, productId?, price?, currency?, appUserId?, referralCode? }
await InfluTo.reportPurchase({ platform: 'ios', signedTransaction: jws });          // StoreKit 2 JWS
await InfluTo.reportPurchase({ platform: 'android', purchaseToken });              // Play Billing token
```
```swift title="iOS (Swift)"
// static func reportPurchase(verification: VerificationResult<Transaction>, appUserId: String? = nil,
//                            referralCode: String? = nil) async throws -> PurchaseResult
// static func reportPurchase(jws: String, appUserId: String? = nil,
//                            referralCode: String? = nil) async throws -> PurchaseResult
let outcome = try await product.purchase(options: [.appAccountToken(InfluTo.appAccountToken(forUserID: userID))])
if case .success(let verification) = outcome {
    _ = try await InfluTo.reportPurchase(verification: verification, appUserId: userID)
    // finish the transaction as usual
}
```
```kotlin title="Android (Kotlin)"
// suspend fun reportPurchase(purchaseToken: String, appUserId: String? = null, referralCode: String? = null,
//                            productId: String? = null, price: Double? = null, currency: String? = null): PurchaseResult
InfluTo.reportPurchase(purchaseToken = purchase.purchaseToken)                       // subscription
InfluTo.reportPurchase(purchaseToken = purchase.purchaseToken,
                       productId = "coins_100", price = 4.99, currency = "USD")       // one-time product
```
```dart title="Flutter (Dart)"
// Future<PurchaseResult> reportPurchase({required String platform, String? signedTransaction,
//     String? purchaseToken, String? productId, double? price, String? currency,
//     String? appUserId, String? referralCode})
await InfluTo.instance.reportPurchase(platform: 'android', purchaseToken: token);
```
```ts title="Web / Capacitor / PWA"
// reportPurchase(options: ReportPurchaseOptions): Promise<PurchaseResult>
await InfluTo.reportPurchase({ platform: 'android', purchaseToken });
```
<!-- /tabs -->

Returns a [PurchaseResult](#purchaseresult).

| Option | Meaning |
|---|---|
| `platform` | `ios` or `android`. Defaults to the current platform on React Native and in Capacitor/Cordova; required on Flutter and in a plain browser. iOS and Android set it themselves. |
| `signedTransaction` | iOS: the StoreKit 2 `jwsRepresentation` (the signed JWS, not the decoded JSON). The iOS SDK takes the `VerificationResult` or the JWS string. |
| `purchaseToken` | Android: the Play Billing `purchaseToken`. |
| `productId`, `price`, `currency` | Android one-time products only: the product ID (routes the purchase to one-time validation) and the price paid with its ISO currency, from `ProductDetails`. Omit them for subscriptions. |
| `appUserId` | Defaults to the ID from `identifyUser`. |
| `referralCode` | Defaults to the stored code, so the first transaction binds to the referral. |

- **Throws** on any error: `400` while the platform isn't switched to store-direct (so it's safe to ship first) or when the proof fails verification, `409` for an Android one-time purchase that isn't in the PURCHASED state yet, `503` when the currency rate is unavailable — retry later. Error types per platform are in [Limits and errors](https://docs.influ.to/reference/limits-and-errors.md#sdk-error-types).
- Reporting the same purchase twice is safe; the server keys purchases on Apple's original transaction ID and Google's purchase token.
- iOS: `InfluTo.appAccountToken(forUserID:)` gives a stable token to pass to `product.purchase(options:)` — see [below](#ios-appaccounttokenforuserid).

## Automatic purchase capture

On store-direct platforms the SDKs can report purchases for you. Capture starts only when `/sdk/init` returns `store_direct: true` for the platform and `autoCapture` isn't `false`, so RevenueCat apps are never affected. It also back-fills existing purchases once, and remembers what it sent so nothing is reported twice.

| Platform | Needs | Start / stop | One-shot back-sync |
|---|---|---|---|
| React Native | `expo-iap` or `react-native-iap` installed | Starts in `initialize`; `enableAutoPurchaseCapture({ backSync?, oneTimeProductIds? })`, `disableAutoPurchaseCapture()` | `syncExistingPurchases()` |
| iOS | Nothing (StoreKit 2, iOS 16+) | Starts in `initialize`; `startPurchaseObservation()`, `stopPurchaseObservation()` | `syncExistingPurchases()` |
| Android | `to.influ:android-sdk-billing` | Starts in `initialize`; `InfluToBilling.startObservation(context, oneTimeProductIds)`, `InfluToBilling.stopObservation()` | `InfluToBilling.syncExistingPurchases(context)` |
| Flutter | `influto_iap` | You start it: `InfluToPurchaseObserver(oneTimeProductIds: …).enable()` after `initialize`; `disable()` | `syncExisting()` |
| Web | A `purchaseSource` | Starts in `initialize`; `enableAutoPurchaseCapture(source?, opts?)`, `disableAutoPurchaseCapture()` | `syncExistingPurchases()` |

<!-- tabs -->
```tsx title="React Native / Expo"
// enableAutoPurchaseCapture(opts?: { backSync?: boolean; oneTimeProductIds?: string[] }): Promise<PurchaseSyncResult>
// disableAutoPurchaseCapture(): void
// syncExistingPurchases(): Promise<PurchaseSyncResult>
const { fetched, sent, failed } = await InfluTo.syncExistingPurchases();
```
```swift title="iOS (Swift)"
// static func startPurchaseObservation() async
// static func stopPurchaseObservation() async
// @discardableResult static func syncExistingPurchases() async -> PurchaseSyncResult
let sync = await InfluTo.syncExistingPurchases()
```
```kotlin title="Android (Kotlin)"
// to.influ.sdk.billing.InfluToBilling (artifact to.influ:android-sdk-billing)
// fun startObservation(context: Context, oneTimeProductIds: Set<String> = emptySet())
// fun stopObservation()
// suspend fun syncExistingPurchases(context: Context): PurchaseSyncResult
// suspend fun report(context: Context, purchase: Purchase, oneTime: Boolean = false,
//                    price: Double? = null, currency: String? = null): Boolean
val sync = InfluToBilling.syncExistingPurchases(applicationContext)
```
```dart title="Flutter (Dart)"
// package:influto_iap/influto_iap.dart
// InfluToPurchaseObserver({InAppPurchase? iap, Set<String> oneTimeProductIds = const {}, bool debug = false})
// Future<PurchaseSyncResult> enable({bool backSync = true}); void disable(); Future<PurchaseSyncResult> syncExisting()
final observer = InfluToPurchaseObserver(oneTimeProductIds: {'coins_100'});
final sync = await observer.enable();
```
```ts title="Web / Capacitor / PWA"
// enableAutoPurchaseCapture(source?: PurchaseSource, opts?: { backSync?: boolean; oneTimeProductIds?: string[] }): Promise<PurchaseSyncResult>
// disableAutoPurchaseCapture(): void
// syncExistingPurchases(): Promise<PurchaseSyncResult>
import { capgoNativePurchasesSource } from '@influto/web/capacitor';

await InfluTo.initialize({ apiKey: 'it_…', purchaseSource: capgoNativePurchasesSource(NativePurchases) });
```
<!-- /tabs -->

- The back-sync returns a [PurchaseSyncResult](#purchasesyncresult).
- Android: `InfluToBilling.report(context, purchase, …)` reports a `Purchase` from your own `BillingClient` without a second client; it returns `true` when the purchase was newly reported. `startObservation` runs its own `BillingClient` — use it only when nothing else handles billing.
- Flutter: the observer checks `InfluTo.instance.autoCaptureEnabled`, so call `enable()` after `initialize` has finished.
- Web purchase sources: `capgoNativePurchasesSource(NativePurchases, opts?)` from `@influto/web/capacitor`, `cordovaPurchaseSource(store, opts?)` from `@influto/web/cordova`, and `digitalGoodsSource(opts?)` from `@influto/web/pwa` for Google Play in a Trusted Web Activity. The Digital Goods API has no purchase event, so call `syncExistingPurchases()` after each checkout.
- Android one-time products: list them in `oneTimeProductIds`; a Play purchase doesn't say whether it's a subscription.

## clearAttribution

Forgets the local attribution. For testing; there's no need to call it on logout.

<!-- tabs -->
```tsx title="React Native / Expo"
// clearAttribution(): Promise<void>
await InfluTo.clearAttribution();
```
```swift title="iOS (Swift)"
// static func clearAttribution() async
await InfluTo.clearAttribution()
```
```kotlin title="Android (Kotlin)"
// fun clearAttribution()   (not suspend)
InfluTo.clearAttribution()
```
```dart title="Flutter (Dart)"
// Future<void> clearAttribution()
await InfluTo.instance.clearAttribution();
```
```ts title="Web / Capacitor / PWA"
// clearAttribution(): Promise<void>
await InfluTo.clearAttribution();
```
<!-- /tabs -->

- Removes `@influto/attribution`, `@influto/influto_code` and `@influto/app_user_id`. The web SDK removes the attribution, the code and `@influto/launch_code`, and keeps the user ID.
- Doesn't touch the server, the per-install ID, the `checkAccess` cache or the list of reported purchases.
- The next `checkAttribution` asks the server again. Within the attribution window the server returns the same answer for a device that already claimed a click, so test a fresh referral with a fresh install.

## Web-only helpers

`@influto/web` adds a few methods for runtimes where a referral arrives through a URL.

```ts
// captureLaunchCode(url?: string): Promise<LaunchCode | null>
// handleLaunchUrl(url: string): Promise<LaunchCode | null>
// captureInstallReferrer(referrer: string | null | undefined): Promise<LaunchCode | null>
// getLaunchCode(): Promise<LaunchCode | null>
// attachRevenueCat(binding: RevenueCatBinding | null): void
// getInitResult(): InitResult | null
// isInitialized: boolean   (getter)
import InfluTo from '@influto/web';
import { listenForCapacitorLaunchUrls } from '@influto/web/capacitor';

await listenForCapacitorLaunchUrls(InfluTo);          // forwards appUrlOpen + the cold-start URL
await InfluTo.captureInstallReferrer(referrerString); // Android, first launch, before checkAttribution
```

| Helper | What it does |
|---|---|
| `captureLaunchCode(url?)` | Reads `influto_code` (and `influto_click`) from a URL — by default the current page — and stores it under `@influto/launch_code` for `checkAttribution`. Runs inside `initialize` unless `captureLaunchCode: false`. When it reads the current page, it also removes the parameters from the address bar (unless `cleanUrl: false`). A captured code replaces a stored organic result. |
| `handleLaunchUrl(url)` | The same, for deep-link handlers (Capacitor `appUrlOpen`, Cordova custom schemes). `listenForCapacitorLaunchUrls(sdk, App?)` wires it for you and returns a function that removes the listener. |
| `captureInstallReferrer(referrer)` | Takes the Google Play Install Referrer string (from `@capgo/capacitor-install-referrer` or `cordova-play-install-referrer`). InfluTo's referral links put the code and click ID there, so Android attribution becomes exact. Call it once on first launch, before `checkAttribution`. |
| `getLaunchCode()` | The captured launch code, if any. |
| `attachRevenueCat(binding)` | Sets or replaces the RevenueCat binding after `initialize`. |
| `getInitResult()`, `isInitialized` | The last `/sdk/init` response, and whether `initialize` succeeded. |
| `createInfluTo()` | A separate SDK instance (tests, several apps on one page). Most apps use the default export. |

`LaunchCode` is `{ code, clickId?, capturedAt }`. The root package also exports `InfluToApiError`, `STORAGE_KEYS`, `SDK_VERSION` and the adapter helpers; the React component and hook are documented under [UI components](https://docs.influ.to/reference/ui.md#web-react).

## iOS: appAccountToken(forUserID:)

Returns a stable UUID for a user ID, to pass as the StoreKit `appAccountToken` when the user buys. Not async, no request.

```swift
// static func appAccountToken(forUserID userID: String) -> UUID
let token = InfluTo.appAccountToken(forUserID: userID)
let outcome = try await product.purchase(options: [.appAccountToken(token)])
```

The same user ID always gives the same UUID (derived from a SHA-256 of the ID, in UUID version 5 layout), so the purchase stays tied to the user across renewals.

## Result types

### AttributionResult

Returned by `checkAttribution`. Same field names on every platform.

| Field | Type | Meaning |
|---|---|---|
| `attributed` | boolean | Whether a creator referred this install |
| `referralCode` | string? | The creator's code |
| `attributionMethod` | string? | `link_param`, `ip_device_match` or `manual_entry` |
| `clickedAt` | string? | ISO 8601 time of the referral click (or of the manual entry) |
| `message` | string? | Server or SDK explanation, for example "No matching referral click found (organic install)" |
| `clickId` | string? | Web only: the claimed click |

### CodeValidationResult

Returned by `validateCode` and `applyCode`. React Native and web keep snake_case for the nested fields; iOS, Android and Flutter use camelCase.

| Field (RN / web) | iOS / Android / Flutter | Meaning |
|---|---|---|
| `valid` | `valid` | Whether the code is an active creator code or a valid free-access code |
| `code` | `code` | The matched code as stored by InfluTo |
| `campaign.id`, `.name`, `.description` | same | The campaign. Free-access codes: name `Free Access` |
| `campaign.commission_percentage` | `campaign.commissionPercentage` | Creator commission (0 for free-access codes) |
| `campaign.campaign_type` | `campaign.campaignType` | `public`, `private`, `invite_only`, or `FREE_ACCESS` |
| `influencer.name`, `.social_handle`, `.follower_count` | `influencer.name`, `.socialHandle`, `.followerCount` | The creator's details — personal data, hidden by default in the UI components. `null` for free-access codes |
| `custom_data` | `customData` | Extra data attached to a free-access code |
| `message` | `message` | Success text; for creator codes it includes the creator's name |
| `error` | `error` | Error text |
| `error_code` | `errorCode` | See the table below. Android: enum `CodeErrorCode`, with `UNKNOWN` for unrecognized values |
| `applied` | `applied` | `applyCode` only: whether the code was applied |

| Error code | When |
|---|---|
| `INVALID_FORMAT` | Fewer than 4 or more than 20 characters after cleanup |
| `CODE_NOT_FOUND` | No active creator code or free-access code matches ("Code not found or inactive") |
| `CODE_EXPIRED` | A free-access code that expired or has no seats left |
| `NETWORK_ERROR` | Set by the SDK: the request failed, or the SDK isn't initialized |

### SetCodeResult

Returned by `setReferralCode`. camelCase on every platform.

| Field | Type | Meaning |
|---|---|---|
| `success` | boolean | Whether the server recorded the code |
| `code` | string? | The matched code |
| `message` | string | Server or SDK message (optional on iOS, Android and Flutter) |
| `campaign` | `{ id, name }`? | The campaign the code belongs to |
| `freeAccess` | boolean? | `true` when it's a free-access code |
| `grantsAccess` | boolean? | `true` when the server granted free access to this user |
| `entitlement` | string? | The RevenueCat entitlement mirrored for the grant, when the app has RevenueCat access set up |
| `expiresAt` | string? | ISO 8601 end of the free access; absent for open-ended access |

### AccessResult

Returned by `checkAccess`. camelCase on every platform.

| Field | Type | Meaning |
|---|---|---|
| `hasAccess` | boolean | Whether InfluTo grants this user free access now |
| `source` | string? | `comp` when access comes from a free-access code |
| `entitlement` | string? | The mirrored RevenueCat entitlement, if any |
| `expiresAt` | string? | ISO 8601 end of the access; absent when open-ended |
| `code` | string? | The free-access code that granted it |

### PurchaseResult

Returned by `reportPurchase`.

| Field (RN / web) | iOS / Android / Flutter | Meaning |
|---|---|---|
| `success` | `success` | `true` when the purchase was verified and processed |
| `validated` | `validated` | `apple` or `google` — a string, not a boolean |
| `environment` | `environment` | `PRODUCTION` or `SANDBOX` |
| `event_type` | `eventType` | The event recorded, for example `INITIAL_PURCHASE` or `NON_RENEWING_PURCHASE` |
| `result` | `result` | The processing result, for diagnostics |
| `duplicate`, `recorded` | — | Declared in the web type only (the server always sends them): the purchase was already recorded, or was recorded by this call |

### PurchaseSyncResult

Returned by the back-sync methods: `{ fetched, sent, failed }` — purchases the store returned, newly reported ones (already-reported purchases count as neither sent nor failed), and reports that threw. iOS and Android add `success`, which is `true` when nothing failed.

### Campaign

Returned by `getActiveCampaigns`: `id`, `name`, `description`, and `commission_percentage` (React Native, web) or `commissionPercentage` (iOS, Android, Flutter).

## Local storage

The SDKs keep a few values on the device, under the same keys on every platform: AsyncStorage on React Native, a `UserDefaults` suite (`com.influto.sdk`) on iOS, the `influto_prefs` SharedPreferences file on Android, `shared_preferences` on Flutter, and `localStorage` (or the storage adapter you pass) on web.

| Key | Holds | Cleared by `clearAttribution` |
|---|---|---|
| `@influto/attribution` | The stored attribution result (JSON) | Yes |
| `@influto/influto_code` | The referral or free-access code | Yes |
| `@influto/app_user_id` | The identified user ID | Yes (not on web) |
| `@influto/initialized` | `"true"` after a successful `initialize` | No |
| `@influto/install_id` | A random per-install ID, sent with `track-install` so installs are counted once | No |
| `@influto/access` | The cached positive `checkAccess` result (5 minutes) | No |
| `@influto/launch_code` | Web only: a code captured from the launch URL or Install Referrer | Yes |
| `@influto/reported_purchases` | React Native, Flutter and web: purchases already reported by automatic capture | No |
| `@influto/sent_purchases` | iOS: purchases already reported by automatic capture | No |

On Android, `android-sdk-billing` keeps its list of reported purchases in a separate preferences file, `influto_billing_sync_prefs`. Uninstalling a native app removes all of these, which is why a reinstall counts as a new install.
