> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evomarketing.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile SDK

> Swift Package and React Native package that report app installs and purchases to Dialed

Both SDKs ship from one public repository, [Evo-Marketing-LLC/evo-attribution-sdk](https://github.com/Evo-Marketing-LLC/evo-attribution-sdk), with no dependencies. They report a single install, keep a persistent install ID, and connect later purchases to that ID.

Neither one throws into your app. Every network and storage failure is contained and logged.

## Install

<Tabs>
  <Tab title="iOS (Swift)">
    In Xcode choose **File → Add Package Dependencies…**, paste the package URL, pick a dependency rule starting at `0.1.0`, and add the `EVOAttribution` library to your app target.

    ```text theme={null}
    https://github.com/Evo-Marketing-LLC/evo-attribution-sdk
    ```

    ```swift theme={null}
    import EVOAttribution
    ```

    Requires iOS 15 or later. The package uses only Foundation, UIKit and `os.log`. Source: [`Sources/EVOAttribution`](https://github.com/Evo-Marketing-LLC/evo-attribution-sdk/tree/main/Sources/EVOAttribution) in the SDK repository.
  </Tab>

  <Tab title="React Native">
    Works with Expo and bare React Native. You supply the storage adapter (AsyncStorage, MMKV, or equivalent) and, optionally, a clipboard reader.

    ```bash theme={null}
    npm install @evomarketing/attribution-react-native
    ```

    Package page: [npmjs.com/package/@evomarketing/attribution-react-native](https://www.npmjs.com/package/@evomarketing/attribution-react-native). Source: [`packages/react-native`](https://github.com/Evo-Marketing-LLC/evo-attribution-sdk/tree/main/packages/react-native) in the SDK repository.

    ```ts theme={null}
    import { configureEvoAttribution, trackInstall, trackPurchase } from "@evomarketing/attribution-react-native";
    ```
  </Tab>
</Tabs>

<Note>
  Find your pixel key in the client portal under **Results → Attribution**, in the **Developer setup** section.
</Note>

## Configure

Call this once during app startup, before either tracking method.

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift theme={null}
    EVOAttribution.configure(pixelKey: "pk_your_brand_key")
    ```

    An `endpoint:` argument is available for testing against a non-production API; it defaults to `https://dialedapi.evomarketing.co`.
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    configureEvoAttribution({
      pixelKey: "pk_your_brand_key",
      platform: "android",
      storage: {
        getItem: (key) => AsyncStorage.getItem(key),
        setItem: (key, value) => AsyncStorage.setItem(key, value),
      },
      getClipboard: () => Clipboard.getStringAsync(),
    })
    ```

    `platform` must be `"ios"` or `"android"`. `endpoint` and `appVersion` are optional.
  </Tab>
</Tabs>

## Track the install

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift theme={null}
    _ = await EVOAttribution.trackInstall(readClipboard: true)
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    await trackInstall(true)
    ```
  </Tab>
</Tabs>

`trackInstall` persists an install UUID and reports successfully only once — the flag lives in `UserDefaults` (`evo_install_reported`) on iOS and in your storage adapter on React Native. A network failure leaves it pending, so the next launch retries.

The call sends `pixel_key`, `install_id`, `platform`, the app version, an `occurred_at` timestamp, and the optional clipboard token and creator code.

### The clipboard trade-off

Clipboard reading is **opt-in** because iOS may show a paste notification to the user. It is also the single highest-value signal you can send:

| With the clipboard token                      | Without it                                                                 |
| --------------------------------------------- | -------------------------------------------------------------------------- |
| `clipboard` at 0.95 confidence — payout-grade | Falls back to a creator code (0.9) or a hashed-IP match (0.6, report-only) |

The helper only uses clipboard strings beginning with `evc_`, so it never reads anything else the user copied. On React Native, clipboard support only exists if you passed `getClipboard`.

### Creator codes

Pass the code the new user typed in the app when a link cannot carry the credit — word of mouth, a podcast mention, a screenshot.

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift theme={null}
    _ = await EVOAttribution.trackInstall(readClipboard: true, code: enteredCode)
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    await trackInstall(true, enteredCode)
    ```
  </Tab>
</Tabs>

A clipboard token still wins when both are present. See [Creator codes](/attribution/creator-codes).

## What the install response contains

New installs return HTTP `201`; idempotent retries return `200` with `duplicate: true`.

```json theme={null}
{
  "ok": true,
  "duplicate": false,
  "install": {
    "install_id": "persistent-device-install-id",
    "platform": "ios",
    "attributed": true,
    "resolution_method": "ip",
    "confidence": 0.6,
    "link": { "id": 12, "domain": "getdupe.app", "slug": "" },
    "code": null,
    "creator": { "id": "better-auth-id", "name": "Jane Doe" }
  }
}
```

`resolution_method` is one of `clipboard`, `code`, `ip`, or `unattributed`. `link`, `code`, and `creator` are `null` for an unattributed install. A code-resolved install reports `attributed: true` with `link: null`, the matched `code`, and its creator.

## Track a purchase

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift theme={null}
    await EVOAttribution.trackPurchase(
        transactionId: transaction.id,
        amount: 49.99,
        currency: "USD"
    )
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    await trackPurchase("store-transaction-id", 49.99, "USD")
    ```
  </Tab>
</Tabs>

Purchases post to `POST /api/public/attribution/events` with `source: "sdk"` and the persistent install ID as `external_user_id`. `transactionId` is the server-side deduplication key and should come from the store or order — a retry of the same transaction is a no-op.

A purchase without its own click token inherits the attribution its install already resolved: the same link, creator, code, resolution method, and confidence.

Both methods take an optional trailing `code` argument, same as `trackInstall`.

<Note>
  If you already use RevenueCat, Superwall, Stripe, or Apple's own notifications, you do not need `trackPurchase` — wire the [billing connector](/attribution/billing-connectors) instead and let the provider report revenue server-side.
</Note>

### Sandbox and TestFlight purchases

Purchases reported from a sandbox or TestFlight build must never count as revenue. Pass `sandbox: true` (Swift: `trackPurchase(transactionId:amount:currency:sandbox:)`, or hand the verified StoreKit 2 transaction to `trackPurchase(transaction:amount:currency:)` and the environment is inferred; React Native: the `sandbox` option). Sandbox rows are kept so you can confirm the hookup in the portal, but every total reads live rows only.

## The install ID

The install ID is a UUID generated on first launch and stored under `evo_install_id`. It is public by design, so subscription SDKs can carry it into their webhook payloads.

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift theme={null}
    Purchases.shared.attribution.setAttributes(["evo_install_id": EVOAttribution.installId])
    Superwall.shared.setUserAttributes(["evo_install_id": EVOAttribution.installId])
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    Purchases.setAttributes({ evo_install_id: await getEvoInstallId() })
    Superwall.shared.setUserAttributes({ evo_install_id: await getEvoInstallId() })
    ```
  </Tab>
</Tabs>

Add these right after you configure the provider's SDK, and after `configure`/`configureEvoAttribution` has run. `getEvoInstallId()` throws if EVO attribution was never configured.

### Apple direct (no RevenueCat or Superwall)

Pass the same UUID to StoreKit so Apple's server notification carries it:

```swift theme={null}
try await product.purchase(options: [.appAccountToken(UUID(uuidString: EVOAttribution.installId)!)])
```

StoreKit 1 apps put the value in `payment.applicationUsername` instead. The Apple connector only accepts an `appAccountToken` that is a well-formed UUID.

## Testing

### With curl

```bash theme={null}
curl -i https://dialedapi.evomarketing.co/api/public/attribution/installs \
  -H 'Content-Type: application/json' \
  -d '{"pixel_key":"pk_your_brand_key","install_id":"test-install-1","platform":"ios"}'

curl -i https://dialedapi.evomarketing.co/api/public/attribution/events \
  -H 'Content-Type: application/json' \
  -d '{"pixel_key":"pk_your_brand_key","event_type":"purchase","source":"sdk","external_user_id":"test-install-1","transaction_id":"test-order-1","code":"JANE10","amount":10,"currency":"USD"}'
```

Use a real brand pixel key. Re-running the first command returns `200` with `duplicate: true`.

### On TestFlight

<Steps>
  <Step title="Open a real creator app link on the device">
    Tap **Get the app** so the click token reaches the clipboard, then install the TestFlight build.
  </Step>

  <Step title="Launch and check the install">
    `trackInstall` should return `resolution_method: "clipboard"` at `0.95`. If it returns `ip` or `unattributed`, the clipboard handoff did not survive — usually an in-app browser.
  </Step>

  <Step title="Make a sandbox purchase">
    A StoreKit sandbox purchase flows through your billing connector and flips that connection to **Connected**, without appearing in the client's conversion or revenue totals.
  </Step>
</Steps>

<Warning>
  Purchases reported by `trackPurchase` are **not** flagged as sandbox — the SDK endpoint has no sandbox flag, so a TestFlight purchase reported that way lands as live revenue. Only provider webhooks carry the sandbox marker. Use throwaway amounts and be ready to clean up, or test purchases through a connector instead.
</Warning>

### Resetting a device

`trackInstall` reports once per install. To test it again, delete the app (clearing `UserDefaults`) or clear your storage adapter's `evo_install_id` and `evo_install_reported` keys.
