RevenueCat + web2wave integration

Step 1. Add required info from RevenueCat to web2wave project settings

  • RevenueCat Project ID
  • API keys
  • Entitlement name

Step 2. Add your deeplink

  1. Go to Deeplink tab
  2. Click on help icon on the right to the field to launch Helper
  3. Add base deeplink
  4. Add user properties you want to include
    • Usually just user_id
  5. Final link will be inserted

After the purchase of a subscription on web, user will get this deeplink to install the app.

Step 3. Resolve user_id and send RevenueCat App User ID to web2wave

If you use an MMP (AppsFlyer, Adjust, …) — read user_id from the deeplink as shown below.

If you do not use an MMP — call identify() instead. See web2wave deferred deeplinks.

AppsFlyer example (reads deep_link_value JSON)

  1. Read deeplink_value, extract user_id from JSON
  2. Get RevenueCat App User ID
  3. Send it to web2wave API with web2wave's user_id from deeplink
    1. API endpoint: https://web2-tfsv.readme.io/reference/post_user-properties
    2. iOS SDK – https://github.com/web2wave/web2wave_swift
    3. Flutter SDK – https://github.com/web2wave/web2wave_flutter
    4. Kotlin SDK – https://github.com/web2wave/web2wave_kotlin
    5. Java SDK – https://github.com/web2wave/web2wave_java
import UIKit
import AppsFlyerLib
import Purchases
import Web2Wave

class ViewController: UIViewController, DeepLinkDelegate {
    var userId: String?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        Purchases.configure(withAPIKey: "your_revenuecat_public_api_key") // Initialize RevenueCat
        AppsFlyerLib.shared().deepLinkDelegate = self
        AppsFlyerLib.shared().start() // Set up AppsFlyer SDK
        Web2Wave.shared.apiKey = "your-api-key" // Configure Web2Wave SDK
    }
    
    func didResolveDeepLink(_ result: DeepLinkResult) {
        if case .found = result.status, let deepLink = result.deepLink {
            handleDeepLink(deepLink)
        } else {
            print("Deep link not found or failed: \(String(describing: result.error))")
        }
    }
    
    private func handleDeepLink(_ deepLink: DeepLink) {
        guard let deepLinkValue = deepLink["deep_link_value"] as? String,
              let userData = deepLinkValue.data(using: .utf8),
              let userDict = try? JSONSerialization.jsonObject(with: userData) as? [String: Any],
              let extractedUserId = userDict["user_id"] as? String else {
            print("Failed to parse deep_link_value")
            return
        }
        
        userId = extractedUserId
        print("User ID from deep link: \(extractedUserId)")
        fetchRevenueCatAppUserID()
    }
    
    private func fetchRevenueCatAppUserID() {
        Purchases.shared.getCustomerInfo { customerInfo, error in
            guard let appUserID = customerInfo?.appUserID else {
                print("Failed to fetch RevenueCat user info: \(error?.localizedDescription ?? "Unknown error")")
                return
            }
            print("RevenueCat App User ID: \(appUserID)")
            if let userId = self.userId { Task { await self.sendAppUserIDToWeb2Wave(userId, appUserID) } }
        }
    }
    
    private func sendAppUserIDToWeb2Wave(_ userId: String, _ appUserID: String) async {
        switch await Web2Wave.shared.setRevenuecatProfileID(web2waveUserId: userId, revenueCatProfileID: appUserID) {
        case .success: print("Successfully sent RevenueCat ID to Web2Wave API")
        case .failure(let error): print("Error sending data to Web2Wave API: \(error)")
        }
    }
}
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.appsflyer.AppsFlyerLib
import com.appsflyer.deeplink.DeepLink
import com.appsflyer.deeplink.DeepLinkListener
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import kotlinx.coroutines.*
import web2wave.Web2Wave
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

    private val scope = CoroutineScope(Dispatchers.IO)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Web2Wave.initWith("YOUR_API_KEY_TO_WEB2WAVE")
        
        // Initialize RevenueCat
        Purchases.configure(PurchasesConfiguration.Builder(this, "your_revenuecat_public_api_key").build())
        
        AppsFlyerLib.getInstance().apply {
            init("yourAppsFlyerDevKey", null, this@MainActivity)
            start(this@MainActivity)
            registerConversionListener(this@MainActivity, object : DeepLinkListener {
                override fun onDeepLinking(deepLink: DeepLink?) {
                    deepLink?.deepLinkValue?.let { handleDeepLink(it) }
                }
                override fun onAttributionFailure(error: String?) {
                    println("Failed deep link: $error")
                }
            })
        }
        getSharedPreferences("prefs", Context.MODE_PRIVATE)
            .getString("userId", null)?.let { fetchSubscriptionStatus(it) }
    }

    private fun handleDeepLink(deepLinkValue: String) {
        runCatching {
            JSONObject(deepLinkValue).getString("user_id").also {
                getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().putString("userId", it).apply()
                fetchSubscriptionStatus(it)
                fetchRevenueCatProfileID(it)
            }
        }.onFailure { println("Failed to parse deep link: ${it.message}") }
    }

    private fun fetchSubscriptionStatus(userId: String) {
        scope.launch {
            val isActive = runCatching { Web2Wave.hasActiveSubscription(userId) }.getOrDefault(false)
            runOnUiThread { if (isActive) providePaidContent() else showPaywall() }
        }
    }

    private fun fetchRevenueCatProfileID(userId: String) {
        Purchases.sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback {
            override fun onReceived(customerInfo: CustomerInfo) {
                val revenueCatProfileID = customerInfo.appUserID
                scope.launch {
                    Web2Wave.setRevenuecatProfileID(userId, revenueCatProfileID)
                }
            }
            override fun onError(error: com.revenuecat.purchases.PurchasesError) {
                println("Failed to fetch RevenueCat user info: ${error.message}")
            }
        })
    }

    private fun providePaidContent() = println("Access granted to paid content.")
    private fun showPaywall() = println("Displaying paywall.")
}
import 'package:flutter/material.dart';
import 'package:appsflyer_sdk/appsflyer_sdk.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:web2wave/web2wave.dart';

class DeepLinkHandler extends StatefulWidget {
  @override
  _DeepLinkHandlerState createState() => _DeepLinkHandlerState();
}

class _DeepLinkHandlerState extends State<DeepLinkHandler> {
  String? userId;
  late AppsflyerSdk _appsflyerSdk;

  @override
  void initState() {
    super.initState();
    _initializeSDKs();
  }

  void _initializeSDKs() async {
    await Purchases.setup("your_revenuecat_public_api_key"); // Initialize RevenueCat
    Web2Wave.shared.initialize(apiKey: 'your-api-key'); // Configure Web2Wave SDK

    _appsflyerSdk = AppsflyerSdk(
      AppsFlyerOptions(afDevKey: 'your_dev_key', appId: 'your_app_id')
    );
    _appsflyerSdk.onDeepLink((deepLink) => _handleDeepLink(deepLink));
    _appsflyerSdk.startSDK();
  }

  void _handleDeepLink(Map<dynamic, dynamic> deepLink) {
    final deepLinkValue = deepLink['deep_link_value'];
    if (deepLinkValue != null) {
      try {
        final userDict = Map<String, dynamic>.from(deepLinkValue);
        userId = userDict['user_id'];
        if (userId != null) {
          print("User ID from deep link: $userId");
          _fetchRevenueCatAppUserID();
        }
      } catch (e) {
        print("Failed to parse deep_link_value: $e");
      }
    }
  }

  void _fetchRevenueCatAppUserID() async {
    try {
      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
      String appUserID = customerInfo.originalAppUserId;
      print("RevenueCat App User ID: $appUserID");
      if (userId != null) _sendAppUserIDToWeb2Wave(userId!, appUserID);
    } catch (e) {
      print("Failed to fetch RevenueCat user info: $e");
    }
  }

  Future<void> _sendAppUserIDToWeb2Wave(String userId, String appUserID) async {
    final result = await Web2Wave.shared.setRevenuecatProfileID(
      web2waveUserId: userId, revenuecatProfileId: appUserID
    );

    if (result.isSuccess) {
      print("Successfully sent RevenueCat ID to Web2Wave API");
    } else {
      print("Error sending data to Web2Wave API: ${result.errorMessage}");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Deep Link Handler')),
      body: Center(child: Text('Waiting for deep links...')),
    );
  }
}

Adjust example

Deeplink URL format for Adjust

Adjust campaign links work as web2wave deeplinks — they handle both cases automatically:

  • App already installed → Adjust opens the app directly
  • App not installed (deferred deep link) → Adjust redirects to the App Store; after install, the SDK fires adjustDeferredDeeplinkReceived (iOS) / OnDeferredDeeplinkResponseListener (Android) with the resolved deep link URL

There are two link formats Adjust supports. Both work with web2wave — choose the one that matches your Adjust setup.


Format 1 — Branded link (*.go.link)

Used when you have a custom branded domain set up in Adjust. The deep link routing is configured inside the Adjust dashboard (via adj_deep_link_id). user_id is appended as a top-level query parameter — Adjust automatically passes it through to the resolved app-scheme URL that the SDK delivers to the app.

Example campaign link:

https://yourapp.go.link/?adj_t=1u57dluw&adj_campaign=web2wave&adj_deep_link_id=366866&user_id=USER_ID

In the web2wave Deeplink Helper (Step 2), set the base deeplink to everything up to and including user_id=:

https://yourapp.go.link/?adj_t=TOKEN&adj_campaign=web2wave&adj_deep_link_id=ID&user_id=

web2wave will append the actual user id, producing:

https://yourapp.go.link/?adj_t=TOKEN&adj_campaign=web2wave&adj_deep_link_id=ID&user_id=12345

The SDK receives yourapp://open?user_id=12345 — parse user_id from the query string.


Format 2 — Direct Adjust link (app.adjust.com)

No branded domain needed. The full app-scheme deep link (including user_id) is embedded directly in the campaign URL as a URL-encoded deep_link parameter.

Example campaign link:

https://app.adjust.com/1ugwn0ld?deep_link=myapp%3A%2F%2Fopen%3Fuser_id%3D94ec41f8-5c89-4ab9-be88-b526d9495028

Where deep_link decodes to: myapp://open?user_id=94ec41f8-5c89-4ab9-be88-b526d9495028

In the web2wave Deeplink Helper (Step 2), set the base deeplink with the deep_link value pre-filled up to user_id%3D (URL-encoded =):

https://app.adjust.com/YOUR_TOKEN?deep_link=yourapp%3A%2F%2Fopen%3Fuser_id%3D

web2wave will append the actual user id, producing:

https://app.adjust.com/YOUR_TOKEN?deep_link=yourapp%3A%2F%2Fopen%3Fuser_id%3D12345

The SDK receives yourapp://open?user_id=12345 — parse user_id from the query string.


Key difference between the two formats
Branded (*.go.link)Direct (app.adjust.com)
DomainYour custom branded domainAdjust's domain
Deep link routingConfigured in Adjust dashboardInline in the URL (deep_link param)
user_id placementTop-level query param on the campaign URLEncoded inside the deep_link value
App-side parsingSame — user_id arrives as a query param in the resolved app-scheme URL

In both cases the SDK delivers the same kind of URL to your app and the parsing code below is identical.

Adjust docs:


  1. Read the user_id query parameter from the resolved deep link URL
  2. Get RevenueCat App User ID
  3. Send it to web2wave API with web2wave's user_id from deeplink
    1. API endpoint: https://web2-tfsv.readme.io/reference/post_user-properties
    2. iOS SDK – https://github.com/web2wave/web2wave_swift
    3. Flutter SDK – https://github.com/web2wave/web2wave_flutter
    4. Kotlin SDK – https://github.com/web2wave/web2wave_kotlin
    5. Java SDK – https://github.com/web2wave/web2wave_java
import UIKit
import Adjust
import Purchases
import Web2Wave

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, AdjustDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Purchases.configure(withAPIKey: "your_revenuecat_public_api_key")
        Web2Wave.shared.apiKey = "your-api-key"

        let adjustConfig = ADJConfig(appToken: "your_adjust_app_token", environment: ADJEnvironmentProduction)
        adjustConfig?.delegate = self
        Adjust.appDidLaunch(adjustConfig)

        return true
    }

    // Handle deferred deep link (user installs app after clicking the web link)
    func adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool {
        guard let deeplink = deeplink else { return false }
        handleDeepLink(deeplink)
        return true
    }

    // Handle direct deep link via Universal Links
    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let url = userActivity.webpageURL {
            if let deeplink = ADJDeeplink(deeplink: url) {
                Adjust.processDeeplink(deeplink)
            }
            handleDeepLink(url)
        }
        return true
    }

    private func handleDeepLink(_ url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let userId = components.queryItems?.first(where: { $0.name == "user_id" })?.value else {
            print("Failed to extract user_id from deep link")
            return
        }
        print("User ID from deep link: \(userId)")
        fetchRevenueCatAppUserID(userId: userId)
    }

    private func fetchRevenueCatAppUserID(userId: String) {
        Purchases.shared.getCustomerInfo { customerInfo, error in
            guard let appUserID = customerInfo?.appUserID else {
                print("Failed to fetch RevenueCat user info: \(error?.localizedDescription ?? "Unknown error")")
                return
            }
            print("RevenueCat App User ID: \(appUserID)")
            Task { await self.sendAppUserIDToWeb2Wave(userId, appUserID) }
        }
    }

    private func sendAppUserIDToWeb2Wave(_ userId: String, _ appUserID: String) async {
        switch await Web2Wave.shared.setRevenuecatProfileID(web2waveUserId: userId, revenueCatProfileID: appUserID) {
        case .success: print("Successfully sent RevenueCat ID to Web2Wave API")
        case .failure(let error): print("Error sending data to Web2Wave API: \(error)")
        }
    }
}
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.adjust.sdk.Adjust
import com.adjust.sdk.AdjustConfig
import com.adjust.sdk.OnDeferredDeeplinkResponseListener
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import web2wave.Web2Wave

class MainActivity : AppCompatActivity() {

    private val scope = CoroutineScope(Dispatchers.IO)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Web2Wave.initWith("YOUR_API_KEY_TO_WEB2WAVE")
        Purchases.configure(PurchasesConfiguration.Builder(this, "your_revenuecat_public_api_key").build())

        val adjustConfig = AdjustConfig(this, "your_adjust_app_token", AdjustConfig.ENVIRONMENT_PRODUCTION)
        adjustConfig.setOnDeferredDeeplinkResponseListener(OnDeferredDeeplinkResponseListener { deeplink ->
            deeplink?.let { handleDeepLink(it) }
            true // return true to open the deep link
        })
        Adjust.onCreate(adjustConfig)

        // Handle direct deep link if app was opened via intent
        intent?.data?.let { handleDeepLink(it) }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        intent.data?.let { handleDeepLink(it) }
        Adjust.onNewIntent(intent)
    }

    private fun handleDeepLink(uri: Uri) {
        val userId = uri.getQueryParameter("user_id")
        if (userId.isNullOrEmpty()) {
            println("Failed to extract user_id from deep link")
            return
        }
        println("User ID from deep link: $userId")
        fetchRevenueCatProfileID(userId)
    }

    private fun fetchRevenueCatProfileID(userId: String) {
        Purchases.sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback {
            override fun onReceived(customerInfo: CustomerInfo) {
                val revenueCatProfileID = customerInfo.appUserID
                scope.launch {
                    Web2Wave.setRevenuecatProfileID(userId, revenueCatProfileID)
                }
            }
            override fun onError(error: PurchasesError) {
                println("Failed to fetch RevenueCat user info: ${error.message}")
            }
        })
    }
}
import 'package:flutter/material.dart';
import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_config.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:web2wave/web2wave.dart';

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _initializeSDKs();
  }

  void _initializeSDKs() async {
    await Purchases.setup("your_revenuecat_public_api_key");
    Web2Wave.shared.initialize(apiKey: 'your-api-key');

    AdjustConfig adjustConfig = new AdjustConfig(
      'your_adjust_app_token',
      AdjustEnvironment.production,
    );

    // Handle deferred deep link (user installs app after clicking the web link)
    adjustConfig.deferredDeeplinkCallback = (String? deeplink) {
      if (deeplink != null) {
        _handleDeepLink(Uri.parse(deeplink));
      }
      return true; // return true to open the deep link
    };

    Adjust.start(adjustConfig);
  }

  void _handleDeepLink(Uri uri) {
    final userId = uri.queryParameters['user_id'];
    if (userId == null || userId.isEmpty) {
      print('Failed to extract user_id from deep link');
      return;
    }
    print('User ID from deep link: $userId');
    _fetchRevenueCatAppUserID(userId);
  }

  void _fetchRevenueCatAppUserID(String userId) async {
    try {
      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
      String appUserID = customerInfo.originalAppUserId;
      print('RevenueCat App User ID: $appUserID');
      _sendAppUserIDToWeb2Wave(userId, appUserID);
    } catch (e) {
      print('Failed to fetch RevenueCat user info: $e');
    }
  }

  Future<void> _sendAppUserIDToWeb2Wave(String userId, String appUserID) async {
    final result = await Web2Wave.shared.setRevenuecatProfileID(
      web2waveUserId: userId,
      revenuecatProfileId: appUserID,
    );

    if (result.isSuccess) {
      print('Successfully sent RevenueCat ID to Web2Wave API');
    } else {
      print('Error sending data to Web2Wave API: ${result.errorMessage}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Deep Link Handler')),
        body: Center(child: Text('Waiting for deep links...')),
      ),
    );
  }
}

Restore purchases in 1-2 seconds: https://www.revenuecat.com/docs/getting-started/restoring-purchases

Purchases.shared.restorePurchases { customerInfo, error in
    // ... check customerInfo to see if entitlement is now active
}

Purchases.sharedInstance.restorePurchasesWith() { customerInfo ->
	//... check customerInfo to see if entitlement is now active
}

Step 4. The entitlement will be granted to the user on RevenueCat

All subscription changes – pauses, cancellations, will be reflected on the user later.