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

# Web Funnel — Redeem web purchases in your app

> Let users who purchased on your web funnel unlock the subscription inside your mobile app via a redemption deep link. Grant-first: the entitlement is already granted server-side; the SDK just refreshes it.

A **Web Funnel** lets a user buy a subscription on the web (Stripe Checkout) and then unlock it inside your mobile app. After a successful purchase, Qonversion emails the buyer a one-time **redemption link**. When they tap it on their device, your app opens and the SDK redeems the purchase — activating the entitlement for that device.

This guide covers the mobile side: setting up the deep link and handling redemption in the Qonversion SDK.

<Info>
  **Grant-first model.** On a successful redemption the backend has **already granted** the entitlement to the app user. The SDK only **refreshes** the local entitlements — you do **not** need to call `identify()` or grant anything yourself.
</Info>

## How it works

1. The user completes a purchase on your published web funnel.
2. Qonversion sends a redemption email with a link of the shape:
   `https://screens.qonversion.io/r/{project_uid}/{token}`
3. The user taps the link on their phone → your app opens via a Universal Link (iOS) / App Link (Android).
4. Your app passes the link to the Qonversion SDK, which redeems the token and refreshes entitlements.

<Info>
  **Opened on desktop or without the app?** The same URL also serves a minimal **"Open in app"** fallback page (for desktop browsers, or if the Universal/App Link didn't auto-open). Visiting that page does **not** consume the token — it's only consumed when the app redeems it. So the link keeps working until the user opens it on a device with the app installed (within the 30-day window).
</Info>

## Before you start

* **Qonversion SDK installed and initialized** in your app. See [Install SDK](installation). Redemption works in the SDK's default Subscription Management mode.
* The device has a Qonversion user (the SDK creates one on launch). Redemption grants the entitlement to **this** device's user — there's nothing to identify manually.

## Prerequisites

Before redemption links can open your app, register your app in the Qonversion Dashboard so Qonversion can serve the association files (`apple-app-site-association` / `assetlinks.json`) for `screens.qonversion.io`:

**Dashboard → Project Settings → Connect Apps** — add your app:

* **iOS:** Bundle ID + Apple Team ID
* **Android:** Package name + signing certificate **SHA-256** fingerprint

Qonversion generates the association files automatically from these values. No manual file hosting is required.

## Step 1 — Register the deep link

The redemption link uses the shared host `screens.qonversion.io`, and the path is scoped to **your** project: `/r/{project_uid}/{token}`. You can find your `project_uid` in the Dashboard (Connect Apps).

<Warning>
  **iOS and Android differ here**

  * **iOS** — you register only the **domain**; Qonversion scopes the path to your project server-side.
  * **Android** — you must register the **path scoped to your `project_uid`**. Because the domain is shared across all Qonversion merchants, a generic `/r/*` filter would clash with other Qonversion-powered apps on the device. Scoping to your `project_uid` prevents that.
</Warning>

### iOS — Associated Domains

Add the Associated Domains capability with:

```
applinks:screens.qonversion.io
```

### Android — App Links

Add an intent filter to the activity that should handle redemption, scoped to your `project_uid`:

```xml AndroidManifest.xml theme={null}
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="https"
        android:host="screens.qonversion.io"
        android:pathPattern="/r/YOUR_PROJECT_UID/.*" />
</intent-filter>
```

Replace `YOUR_PROJECT_UID` with your project's uid from the Dashboard.

## Step 2 — Handle the redemption link

Forward the incoming URL to `handleRedemptionLink`. The SDK validates the host and path internally before making any network call.

<CodeGroup>
  ```swift Swift theme={null}
  // SwiftUI — forward the Universal Link
  .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
      guard let url = activity.webpageURL else { return }
      Qonversion.shared().handleRedemptionLink(url: url) { result in
          switch result {
          case .success:
              print("Entitlements refreshed")
          case .tokenExpired, .alreadyConsumed, .invalidToken:
              print("Redemption not completed: \(result)")
          case .networkError, .retryable:
              print("Temporary error — try again")
          @unknown default:
              break
          }
      }
  }
  ```

  ```objectivec Objective-C theme={null}
  // UIKit — scene:continueUserActivity: / application:continueUserActivity:
  [[Qonversion sharedInstance] handleRedemptionLink:url completion:^(QONRedemptionResult result) {
      if (result == QONRedemptionResultSuccess) {
          NSLog(@"Entitlements refreshed");
      }
  }];
  ```

  ```kotlin Kotlin theme={null}
  // Android — in the activity that receives the App Link
  override fun onNewIntent(intent: Intent) {
      super.onNewIntent(intent)
      val uri = intent.data ?: return
      Qonversion.shared.handleRedemptionLink(uri, object : QonversionRedemptionCallback {
          override fun onResult(result: RedemptionResult) {
              when (result) {
                  RedemptionResult.Success -> { /* entitlements refreshed */ }
                  RedemptionResult.TokenExpired,
                  RedemptionResult.AlreadyConsumed,
                  RedemptionResult.InvalidToken -> { /* not completed */ }
                  RedemptionResult.NetworkError,
                  RedemptionResult.Retryable -> { /* temporary — retry */ }
              }
          }
      })
  }
  ```
</CodeGroup>

<Warning>
  Do not call `identify()` after a successful redemption. The entitlement is already granted to the user on the backend; the SDK refreshes it for you. Adding your own `identify()` is unnecessary and may cause an extra network round-trip.
</Warning>

## Redemption results

| Result            | Meaning                                                   | Suggested handling                                                       |
| :---------------- | :-------------------------------------------------------- | :----------------------------------------------------------------------- |
| `Success`         | Entitlement granted and refreshed.                        | Continue into the app's paid experience.                                 |
| `TokenExpired`    | The link's TTL elapsed (30 days by default).              | Offer to reissue the link (see below).                                   |
| `AlreadyConsumed` | The link was already redeemed.                            | Inform the user; the purchase is already active on the redeeming device. |
| `InvalidToken`    | Malformed / unknown token.                                | Show a generic error.                                                    |
| `NetworkError`    | The device could not reach the server.                    | Ask the user to retry.                                                   |
| `Retryable`       | The server returned a transient error (rate limit / 5xx). | Retry later.                                                             |

## Reissue a redemption link

If the original link expired or was lost, let the user request a fresh one by email. The two platforms expose this differently:

* **iOS** — your app collects the email and passes it to `reissueRedemption(email:)`.
* **Android** — the SDK presents a ready-made dialog that collects the email for you via `presentReissueUI(activity:)`.

<CodeGroup>
  ```swift Swift theme={null}
  Qonversion.shared().reissueRedemption(email: email) { success, statusCode, error in
      // success == a new redemption email was accepted for sending
  }
  ```

  ```kotlin Kotlin theme={null}
  // The SDK shows a dialog that collects the email and requests the new link.
  Qonversion.shared.presentReissueUI(activity) { success ->
      // success == a new redemption email was accepted for sending
  }
  ```
</CodeGroup>

<Info>
  A successful/accepted result means the request was **accepted**, not that a matching purchase exists or that the mail was delivered — the backend intentionally responds uniformly so the endpoint can't be used to probe which emails have purchases.
</Info>
