Step 1. Add required info from RevenueCat to web2wave project settings
- RevenueCat Project ID
- API keys
- Entitlement name
Step 2. Add your deeplink
- Go to Deeplink tab
- Click on help icon on the right to the field to launch Helper
- Add base deeplink
- Add user properties you want to include
- Usually just user_id
- 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
user_id and send RevenueCat App User ID to web2waveIf 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)
deep_link_value JSON)- Read deeplink_value, extract user_id from JSON
- Get RevenueCat App User ID
- Send it to web2wave API with web2wave's user_id from deeplink
- API endpoint: https://web2-tfsv.readme.io/reference/post_user-properties
- iOS SDK – https://github.com/web2wave/web2wave_swift
- Flutter SDK – https://github.com/web2wave/web2wave_flutter
- Kotlin SDK – https://github.com/web2wave/web2wave_kotlin
- 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)
*.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)
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) | |
|---|---|---|
| Domain | Your custom branded domain | Adjust's domain |
| Deep link routing | Configured in Adjust dashboard | Inline in the URL (deep_link param) |
user_id placement | Top-level query param on the campaign URL | Encoded inside the deep_link value |
| App-side parsing | Same — 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:
- Set up deferred deep linking – iOS
- Set up deep linking – Android
- Enable deep links in Adjust dashboard
- Read the
user_idquery parameter from the resolved deep link URL - Get RevenueCat App User ID
- Send it to web2wave API with web2wave's user_id from deeplink
- API endpoint: https://web2-tfsv.readme.io/reference/post_user-properties
- iOS SDK – https://github.com/web2wave/web2wave_swift
- Flutter SDK – https://github.com/web2wave/web2wave_flutter
- Kotlin SDK – https://github.com/web2wave/web2wave_kotlin
- 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.
