> For the complete AppRefer docs index, see https://apprefer.com/llms.txt (every page as markdown) or https://apprefer.com/llms-full.txt (all docs in one file).

# iOS SDK

Native Swift package for attribution, product events, SKAdNetwork and AdAttributionKit conversion values, AppRefer Billing (StoreKit 2) and the in-app Support chat. No third-party dependencies.

Every page in this section follows the same order, so you can switch between the [iOS](https://apprefer.com/docs/sdk/ios.md), [Android](https://apprefer.com/docs/sdk/android.md), [Flutter](https://apprefer.com/docs/sdk/flutter.md) and [React Native](https://apprefer.com/docs/sdk/react-native.md) references and find the same thing in the same place. All four SDKs share one version number; this page documents **0.13.1**.

## Install

Add the package with Swift Package Manager.

1.  In Xcode, choose **File → Add Package Dependencies…**
2.  Enter `https://github.com/AppAgentic/apprefer-ios-sdk`
3.  Choose **Up to Next Minor Version** from `0.13.1` and add the `AppRefer` library to your app target.

**Package.swift**

```swift
dependencies: [
    .package(url: "https://github.com/AppAgentic/apprefer-ios-sdk", from: "0.13.1")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [.product(name: "AppRefer", package: "apprefer-ios-sdk")]
    )
]
```

> **Pre-1.0 versions**
>
> Before 1.0, a minor version can change behaviour. Pin to the minor version (`.upToNextMinor(from: "0.13.1")`) and read the changelog highlights below before upgrading.

### Platform setup

-   The SDK links Apple's `AdServices`, `AdSupport` and `AppTrackingTransparency` frameworks itself. No entitlements or capabilities are needed for attribution.
-   In your privacy manifest (`PrivacyInfo.xcprivacy`), declare **only** `trk.apprefer.com` under `NSPrivacyTrackingDomains`. Never add `apprefer.com`: it serves the first-party requests that must keep working when the user hasn't allowed tracking. See [Tracking consent](#tracking-consent).
-   To receive SKAdNetwork and AdAttributionKit postback copies, add the Info.plist keys in [SKAdNetwork and AdAttributionKit](#skadnetwork-and-adattributionkit).

**PrivacyInfo.xcprivacy**

```xml
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array>
    <string>trk.apprefer.com</string>
</array>
```

## Configure

Call `configure` once, as early as possible at launch. On the first launch it sends the install's signals to AppRefer and resolves attribution; on later launches it returns the cached result without a network call. Calling it again with the same key reuses the existing instance.

**MyApp.swift**

```swift
import SwiftUI
import AppRefer

@main
struct MyApp: App {
    init() {
        Task {
            let attribution = await AppRefer.configure(apiKey: "pk_live_...")
            if let attribution {
                print(attribution.network, attribution.matchType)
            }
        }
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
```

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `apiKey` | String | required | SDK key from the dashboard (`pk_live_…` or `pk_test_…`) |
| `userId` | String? | nil | Your app's user id, if someone is already signed in |
| `debug` | Bool | false | Print SDK logs to the console |
| `logLevel` | Int | 1 | 0 none, 1 errors, 2 warnings, 3 verbose |

`configure` returns `Attribution?` and never throws. Every SDK method uses `await` without `try`, except the Billing and Support calls that say so. Methods called before `configure` do nothing.

> **Live and test keys**
>
> The server picks the environment from the key. `pk_test_` installs, events and purchases are sandbox data: visible in the dashboard's sandbox view and Debugger, never forwarded to ad networks. Use `pk_live_` in release builds.

### configureResult

`configureResult` takes the same parameters and tells you why there is or isn't an attribution. It reports organic only after the server has said so, never because the network failed.

```swift
switch await AppRefer.configureResult(apiKey: "pk_live_...") {
case .attributed(let attribution): showWelcome(for: attribution.campaignName)
case .pending: break   // waiting for the user's ATT answer (see Tracking consent)
case .organic: break   // the server answered: no ad click matched
case .failed(let error): print(error)  // offline or unavailable; try again next launch
}
```

| Result | Meaning |
| --- | --- |
| `.attributed(Attribution)` | Matched to an ad click, Apple Ads or a Play referrer |
| `.pending` | The server is holding the decision until the user answers the ATT prompt (up to 24 hours) |
| `.organic` | Confirmed organic install |
| `.failed(Error)` | No answer from the server and nothing cached |

`AppRefer.getAttribution()` returns the cached attribution at any time without a network call.

## Attribution result

The `Attribution` struct returned by `configure` and `getAttribution()`:

| Property | Type | Description |
| --- | --- | --- |
| `network` | String | Ad network, e.g. `meta`, `google`, `tiktok`, `apple_ads`, `openai`, or `organic` |
| `matchType` | String | How the install was matched, e.g. `adservices`, `ip_match`, `probabilistic`, `organic` (see [Attribution](https://apprefer.com/docs/attribution.md)) |
| `attributionId` | String? | AppRefer's id for this attribution |
| `campaignId` | String? | Campaign id from the ad network or tracking link |
| `campaignName` | String? | Campaign name |
| `adGroupId` | String? | Ad set / ad group id |
| `adId` | String? | Ad id |
| `keyword` | String? | Search keyword (Apple Ads, Google) |
| `fbclid` | String? | Meta click id, when matched to a Meta click |
| `gclid` | String? | Google click id |
| `ttclid` | String? | TikTok click id |
| `campaign` | String? | Older field kept for compatibility; use `campaignName` |
| `queryParams` | \[String: String\] | Extra tracking-link parameters, when present |
| `customData` | \[String: String\] | Custom data, when present |
| `trackingDecisionPending` | Bool | True while the server waits for the ATT answer |
| `createdAt` | Date | When the attribution was made |

## Tracking consent

AppRefer keeps cross-company tracking and first-party work apart for each install. The SDK reads the App Tracking Transparency status your app already has. **It never shows the ATT prompt.**

| ATT status | Host | What AppRefer does |
| --- | --- | --- |
| Authorized | `trk.apprefer.com` | Matches the install to its ad click, forwards conversions to your ad networks, uses advanced matching |
| Denied or restricted | `apprefer.com` | Apple Ads (AdServices) or organic attribution only; analytics, Billing and Support keep working; nothing is forwarded |
| Not determined | `apprefer.com` | Treated as denied until the user answers; the decision is held for up to 24 hours in case they allow tracking |

If you show the ATT prompt, tell AppRefer straight after it. The SDK also re-checks on every `configure` and whenever the app becomes active, because the user can change it in Settings.

```swift
import AppTrackingTransparency

_ = await ATTrackingManager.requestTrackingAuthorization()
await AppRefer.trackingConsentDidChange()

let consent = AppRefer.trackingConsent()   // .granted, .denied or .undetermined
```

The IDFA is read only when ATT is authorized. Your app needs an `NSUserTrackingUsageDescription` only if it shows the prompt. More on how consent changes matching: [Privacy](https://apprefer.com/docs/privacy.md).

## User identity

Tell AppRefer who is signed in, so Support conversations and product events carry the account (`app_user_id`). Use the same id you pass to `AppReferBilling.logIn`. You can also pass it as `userId` to `configure`.

```swift
await AppRefer.setUserId(currentUser.id)
try await AppReferBilling.logIn(currentUser.id)   // if you use AppRefer Billing
```

### Advanced matching

Send hashed customer details to improve Meta match rates. Values are SHA-256 hashed on the device; nothing is sent unless ATT is authorized. Call it once after sign-up or sign-in.

```swift
await AppRefer.setAdvancedMatching(
    email: "user@example.com",
    phone: "+15551234567",
    firstName: "Jane",
    lastName: "Doe",
    dateOfBirth: "19900131"   // YYYYMMDD
)
```

### Device id

`await AppRefer.getDeviceId()` returns AppRefer's install id. It is kept in the Keychain, so it survives a reinstall, and is `nil` while the Keychain can't be read. AppRefer Billing sends it with purchases automatically. If you take payments through RevenueCat instead, set it as the `appreferId` subscriber attribute (see [RevenueCat](https://apprefer.com/docs/integrations/revenuecat.md)).

## Events

Track the in-app moments that matter to you (sign-up, onboarding finished, first workout). Don't track purchases here: AppRefer Billing records purchases, renewals and refunds and forwards them for you. Which events reach ad networks is covered in [Events](https://apprefer.com/docs/events.md).

```swift
await AppRefer.trackEvent("signup")
await AppRefer.trackEvent("tutorial_complete", properties: ["step": "final"])

// Need to know what happened to it?
let result = await AppRefer.trackEventResult("level_up", properties: ["level": 3])
// .sent (server acknowledged), .queued (saved, sent later) or .dropped
```

| Parameter | Type | Description |
| --- | --- | --- |
| `_ eventName` | String | Event name, e.g. `signup` |
| `properties` | \[String: Any\]? | JSON-compatible metadata |
| `revenue` | Double? | Amount for revenue events you track yourself |
| `currency` | String? | ISO 4217 code, e.g. `USD` |

### Offline queue

-   Events are saved on the device before they are sent and keep one stable event id, so delivery is **at least once**.
-   The queue holds up to 1,000 events for up to seven days; the oldest are dropped first.
-   Network errors, HTTP 408, 429 and 5xx are retried with backoff (up to an hour, or longer if the server asks), at launch and when the app returns to the foreground. Other 4xx answers are final.
-   `await AppRefer.clearPendingEvents()` cancels event requests and empties the queue, for example when the user withdraws analytics consent.

## Collection control

`setCollectionEnabled(false)` stops attribution, consent reports, conversion values and product events, cancels what is in flight and clears the event queue. The choice is saved across launches. Billing and Support keep working, without the install's device id.

```swift
await AppRefer.setCollectionEnabled(false)   // consent withdrawn

// Later, to resume: enable, then configure again
await AppRefer.setCollectionEnabled(true)
await AppRefer.configure(apiKey: "pk_live_...")
```

Enabling alone sends nothing. Calling it with `true` while collection is already on changes nothing, so it is safe to call from a consent tool on every launch.

## Account switching

Call `reset()` when a user signs out, and await it before the next account signs in. It revokes the old Support session and its push token, clears the Support profile, history and drafts, the user id and queued events. The install's own attribution and device id stay.

```swift
// Sign out
try? await AppReferBilling.logOut()   // if you use AppRefer Billing
await AppRefer.reset()

// Next account signs in
await AppRefer.configure(apiKey: "pk_live_...", userId: nextUser.id)
try await AppReferBilling.logIn(nextUser.id)
```

-   After `reset()`, attribution and events are paused until you call `configure` again. Events tracked in between are dropped, never sent later.
-   Billing logout stays explicit. Until Billing's `logIn` or `logOut` runs, the Billing user who was current at reset gets no install device id, even after a relaunch (0.9.3+).
-   `reset()` is safe to call before `configure` and more than once. It doesn't change the collection setting.

## SKAdNetwork and AdAttributionKit

Apple's own attribution works for every install, including users who don't allow tracking, but Apple only sends a postback once the app has set a conversion value. The SDK (0.7.0+) does this for you:

-   On first launch it registers the install (value 0, coarse `low`).
-   It raises the value from AppRefer's own data: **10** (`medium`) once a free trial starts, **20 + whole dollars paid** (`high`, capped at 63) once the user pays. Values only go up.
-   It checks at launch, when the app becomes active, after trial and revenue events and after AppRefer Billing purchases, for the 35 days Apple's postbacks cover. It updates AdAttributionKit (iOS 17.4+) and SKAdNetwork (iOS 15.4+; SKAdNetwork 3 on 15.0 to 15.3).

If you take payments outside AppRefer Billing, call this after a purchase to update the value sooner:

```swift
await AppRefer.updateConversionValue()
```

To get copies of the winning postbacks in AppRefer, add these keys to Info.plist, plus the `SKAdNetworkItems` identifiers your ad networks publish:

**Info.plist**

```xml
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://apprefer.com</string>
<key>AttributionCopyEndpoint</key>
<string>https://apprefer.com</string>
```

Postback reporting is described in [SKAdNetwork](https://apprefer.com/docs/integrations/skan.md).

## AppRefer Billing

`AppReferBilling` runs App Store subscriptions and one-time purchases on StoreKit 2. AppRefer verifies each signed transaction, keeps the customer's entitlements and sends the conversion to your ad networks from the same attribution, so no separate purchases SDK or webhook is needed. Billing must be turned on for the app in AppRefer first; see [Billing](https://apprefer.com/docs/billing.md) for the server setup and the move from RevenueCat.

### Set up

```swift
Task { await AppRefer.configure(apiKey: "pk_live_...") }

// Same SDK key. Starts the StoreKit transaction listener right away.
AppReferBilling.configure(
    appUserId: nil,   // nil = anonymous "$ARAnon:<UUID>", kept in the Keychain
    entitlementsByProduct: [
        "pro_monthly": ["premium"],
        "pro_yearly": ["premium"],
    ]
)
```

| Parameter | Default | Description |
| --- | --- | --- |
| `apiKey` | the configure key | SDK key, if different |
| `appUserId` | nil | Your user id; `nil` starts anonymous |
| `entitlementsByProduct` | \[:\] | Product id to entitlement ids, used only when AppRefer can't be reached |
| `defaultEntitlement` | nil | Entitlement for unmapped products (the server default is `premium`) |
| `debug` | `false` | Console logs |
| `logLevel` | 1 | 0 to 3, as for configure |

### Offerings and paywalls

Offerings let you change what a paywall sells from the dashboard (packages, placements, targeting and experiments) without hard-coding product ids. See [Offerings](https://apprefer.com/docs/offerings.md).

```swift
let offerings = try await AppReferBilling.offerings()

if let offering = offerings.currentOffering(forPlacement: "onboarding"),
   let annual = offering.annual {
    priceLabel.text = annual.storeProduct.displayPrice
    let result = try await AppReferBilling.purchase(package: annual)
}
```

-   `offerings.current` is the resolved default and `offerings.all` holds every offering by id. Each offering has `availablePackages`, `metadata`, `package(identifier:)` and the shortcuts `weekly`, `monthly`, `twoMonth`, `threeMonth`, `sixMonth`, `annual` and `lifetime`.
-   An unknown placement falls back to the current offering; a placement set to no offering returns `nil` (show no paywall).
-   Buying through `purchase(package:)` records the offering, placement and experiment with the purchase.
-   Offerings are cached for five minutes (memory and disk). Offline, the last copy is used; with no copy at all the call throws, so keep your existing product ids as a fallback.

### Products, purchase and restore

```swift
let products = try await AppReferBilling.products(["pro_monthly", "pro_yearly"])   // StoreKit Products, in this order

let result = try await AppReferBilling.purchase(products[1])
switch result.outcome {
case .purchased:     unlock(result.customerInfo)
case .userCancelled: break
case .pending:       showAskToBuyNote()   // arrives later through customerInfoStream
}

let info = try await AppReferBilling.restorePurchases()   // AppStore.sync(), then sends every current entitlement
```

-   `purchase` tags the transaction with the customer's AppRefer account token (`appAccountToken`), sends it to AppRefer and only then finishes it.
-   Signed promotional offers: `purchase(product, promotionalOffer: .init(offerId:keyId:nonce:signature:timestamp:))`. Extra StoreKit options go in `options:`.
-   Who is buying is saved before the sheet opens, so a purchase that completes later (Ask to Buy, a restart, a user switch) still goes to them. If that can't be saved, `purchase` throws `AppReferBillingError.purchaseIntentNotSaved` and the sheet doesn't open.
-   Only `restorePurchases()` can move a purchase owned by another app user to the one signed in.
-   `await AppReferBilling.rejectedPurchases()` lists purchases AppRefer refused for good, for your support team.

### Customer info and entitlements

```swift
let info = await AppReferBilling.customerInfo()                          // cached for 5 minutes, then fetched
let fresh = await AppReferBilling.customerInfo(fetchPolicy: .fetchCurrent)

if info.entitlements["premium"]?.isActive == true { /* unlocked */ }

for await info in AppReferBilling.customerInfoStream { update(info) }    // or set AppReferBilling.delegate
```

| Field | Description |
| --- | --- |
| `entitlements` | By id; `.active` lists only active ones |
| `activeSubscriptions` | Product ids of active subscriptions |
| `hasActiveEntitlement` | True when any entitlement is active |
| `appUserId` | The customer the answer is for |
| `source` | `.server`, `.cache` or `.localFallback` |
| `EntitlementInfo` | `isActive`, `productIdentifier`, `expirationDate`, `willRenew`, `periodType`, `store`, `state` |

`customerInfo` never throws. If AppRefer can't be reached, entitlements come from StoreKit's verified current entitlements, mapped through `entitlementsByProduct`, so a paying user is never locked out.

### Users

```swift
try await AppReferBilling.logIn("user_123")   // moves purchases made while anonymous to this user
try await AppReferBilling.logOut()             // new anonymous id

let id = await AppReferBilling.appUserId       // "$ARAnon:..." while anonymous
let anonymous = await AppReferBilling.isAnonymous

await AppReferBilling.setEmail("a@example.com")   // also setDisplayName, setPhoneNumber, setAttributes
```

The install's device id is sent with purchases automatically, so purchases are matched to the ad click without `setUserId`. It is left out while collection is off and after `reset()` until `configure` runs again.

### Sandbox and App Review

-   Test with a sandbox Apple ID. Sandbox purchases are recorded as sandbox, unlock the entitlement and are never forwarded to ad networks or counted as revenue.
-   App Review buys with sandbox accounts in your release build; those purchases work with the live key.
-   A real (production) purchase sent with a `pk_test_` key is refused.
-   Transactions from an Xcode StoreKit configuration file are signed with a local certificate AppRefer rejects. The SDK still unlocks them through its local fallback, but they never reach AppRefer.

## Support

An in-app help chat your team answers from the AppRefer inbox, with screenshots, quick-help topics, reply notifications and a help centre. SwiftUI, iOS 15+, Liquid Glass on iOS 26, 35 languages. Server setup and the inbox are covered in [Support](https://apprefer.com/docs/support.md). `AppRefer.support` is main-actor isolated.

### Open the chat

```swift
AppRefer.support.open()                                  // help home
AppRefer.support.open(conversationId: "cv_...")           // one conversation
AppRefer.support.compose("Problem with my subscription")  // home with the message field filled in

// Or embed it in your own navigation
AppReferSupportView(opening: .home)   // .helpCentre, .article("slug"), .conversation("id")
```

### Identify and customise

```swift
AppRefer.support.identify(name: "Alex", email: "alex@example.com")  // the user id from setUserId is sent automatically
AppRefer.support.accentColor = .purple           // text on a bright accent turns dark
AppRefer.support.title = "Sparkly support"       // default "<App name> support", translated
AppRefer.support.teamNames = ["Sam", "Ana"]      // first names on the home
AppRefer.support.replyTime = "under an hour"     // completes "We usually reply in …"
AppRefer.support.appIcon = UIImage(named: "HelpIcon")
AppRefer.support.topics = [.billing, .bug, .idea,
    AppReferSupportTopic(title: "Restore purchase", subtitle: "Get your plan back",
                         systemImage: "arrow.clockwise", tint: .blue, prompt: "",
                         action: { Task { try? await AppReferBilling.restorePurchases() } })]
AppRefer.support.language = "de"                 // force a language; nil follows the app
AppRefer.support.strings = ["support.home.subtitle": "What can we fix for you?"]
```

Quick-help tiles default to `.billing`, `.restore`, `.bug` and `.idea`; an empty list hides them. A tile with an `action` runs it instead of starting a message. The chat follows the language your app runs in, then the device, then English.

### Reply notifications

AppRefer sends reply notifications through APNs with your key, and emails a reply only if it stays unread for an hour. Register for push once `status().pushReady` is true.

```swift
if (try? await AppRefer.support.status())?.pushReady == true {
    UIApplication.shared.registerForRemoteNotifications()
}

// AppDelegate
func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    AppRefer.support.registerDeviceToken(token)
}

// UNUserNotificationCenterDelegate, when the user taps a notification
if AppRefer.support.handleNotification(response.notification.request.content.userInfo) { return }

let unread = await AppRefer.support.unreadCount()   // for a badge
```

**Push environment (0.10.1+).** The token is registered for the environment the build is signed for, read from the embedded provisioning profile's `aps-environment`: development-signed builds (Debug or Release) and the Simulator use sandbox; App Store, TestFlight, Ad Hoc and Enterprise builds use production. Force one with `AppRefer.support.pushEnvironment = .sandbox` and read the one in use from `effectivePushEnvironment`.

### Help centre

Published help articles appear in the chat with no code (0.10.0+): a "Help articles" card on the home, a searchable help centre and a reader. You can open them directly or build your own screens.

```swift
AppRefer.support.openHelpCentre()
AppRefer.support.open(article: "restore-purchases")

let articles = try await AppRefer.support.articles()                 // [AppReferHelpArticle]
let results = try await AppRefer.support.articles(query: "refund")   // server search, up to 20
do {
    let article = try await AppRefer.support.article(slug: "refunds") // + body (markdown)
} catch let error as AppReferSupportError where error.isNotFound {
    // removed or unpublished
}
```

Articles come in the chat's language and are cached for five minutes; offline, the last copy is shown. Test keys also see drafts.

### Status

```swift
let status = try await AppRefer.support.status()
status.pushReady            // APNs key verified for this build's environment
status.emailReplies         // customers can answer reply emails
status.helpCentreArticles   // number of published articles
```

`status()` follows the server's caching (five minutes once every channel is verified, otherwise 30 seconds), so calling it at every launch is cheap.

### Support without attribution

Apps that want only the chat can start Support on its own. It doesn't configure attribution, collect device signals or send analytics.

```swift
await AppRefer.support.configure(apiKey: "pk_live_...")

// Forget the name, email and user id and start a fresh anonymous session
await AppRefer.support.clearIdentity()
```

## Requirements

| Requirement | Minimum |
| --- | --- |
| iOS | 15.0 (macOS 12, tvOS 15) |
| Swift | 5.10 (Xcode 15.3) |
| Dependencies | None |

## Changelog highlights

| Version | What changed |
| --- | --- |
| 0.13.1 | Narrow-screen Support text wrapping, measured composer spacing and keyboard anchoring. React Native Support-only apps bundle without the optional purchase package. |
| 0.13.0 | Photo/video testimonials from Support, the app or an authenticated coach; usage terms accepted on Submit, signed uploads and receipt-based rewards. See the Support reference for integration. |
| 0.12.0 | Verified customer context, resolution actions, offering and optional review cards, outcomes and tailored Support setup. |
| 0.11.0 | Version parity release. Nothing on this page changes. |
| 0.10.1 | Support push tokens use the build's signing environment instead of the DEBUG flag; new `support.pushEnvironment` and `effectivePushEnvironment`. |
| 0.10.0 | Help centre in the chat; `support.articles`, `article(slug:)`, `status()`, `openHelpCentre()`, `open(article:)`. Chat in 35 languages with `language` and `strings` overrides. |
| 0.9.3 | The Billing account gate set by `reset()` survives a relaunch. |
| 0.9.2 | Billing sends no install device id while collection is off or after `reset()`; new `support.clearIdentity()`. |
| 0.9.1 | Events carry `app_user_id`; `setCollectionEnabled`; after `reset()` call `configure` again. |
| 0.9.0 | `reset()`, Support-only configure, durable event queue, `trackEventResult`, `clearPendingEvents`, `configureResult`. |
| 0.8.0 | Offerings, placements and experiments; `purchase(package:)`. Products mapped to no entitlement unlock nothing offline. |
| 0.7.0 | SKAdNetwork and AdAttributionKit conversion values; `updateConversionValue()`. |
| 0.6.0 | Tracking consent split: `trk.apprefer.com` only for ATT-authorized installs; `trackingConsentDidChange()`, `trackingConsent()`. |

Full history: the `CHANGELOG.md` in [apprefer-ios-sdk](https://github.com/AppAgentic/apprefer-ios-sdk).

---

Source: https://apprefer.com/docs/sdk/ios · Last updated: 2026-10-20
