web2wave deferred deeplinks

Connect a web funnel visitor to their native app install without AppsFlyer, Adjust, Branch, or other MMP — using the identify() method and device matching headers built into every web2wave SDK.

After a user completes a web2wave quiz or paywall in the browser and installs your app, you need their web2wave user_id inside the native app to unlock the subscription.

Most teams get that user_id through a mobile measurement partner (MMP) — AppsFlyer, Adjust, Branch, and similar tools pass it via an install deeplink. That works well if you already run an MMP stack.

If you do not use an MMP, web2wave provides deferred deeplinks as a built-in alternative. Every Mobile SDK sends device-matching headers on API requests and exposes an identify() method (GET /api/user/identify) that matches the installed device against recent web sessions on your project (last 48 hours) and returns the user_id.

No third-party attribution SDK required.

MMP vs web2wave deferred deeplinks

MMP (AppsFlyer, Adjust, Branch, …)web2wave deferred deeplinks
Extra SDKYes — MMP SDK in the appNo — built into web2wave SDK
How user_id arrivesInstall / deferred deeplink from MMPidentify() on first app open
SetupMMP dashboard + deeplink in project settingsInitialize web2wave SDK + call identify()
Best forTeams already using MMP for ads & attributionSimpler stacks, no MMP budget, web-to-app only
📘

These approaches are alternatives, not layers. Pick one attribution path for resolving user_id on install. If you already use AppsFlyer or Adjust, follow the RevenueCat, Adapty, or direct API guides with your MMP deeplink handler.

You can also use web2wave's custom deeplink page without an MMP — but that still requires the user to open the install link again after downloading the app. Deferred deeplinks remove that extra step.

When to use deferred deeplinks

ScenarioRecommended approach
You use AppsFlyer, Adjust, Branch, etc.MMP deeplink — see integration guides above
You want web-to-app attribution without an MMPCall identify() on first app launch
User already known in app (stored user_id)Skip attribution; use stored value

Call identify() once per fresh install, typically on first app open and before linking Adapty / RevenueCat / Qonversion profiles. Matching only considers web sessions from the last 48 hours, so call it as soon as the user opens the app after install.

How deferred deeplinks work

The SDK automatically attaches these HTTP headers to every API call (including identify()):

HeaderExamplePurpose
platformiOS, AndroidDevice family
device_modelPixel 8, SM-S911B, iPhone15,2Hardware model — preferred match signal
screen_size390x844Logical screen resolution — fallback / iOS Safari
timezoneUTC+03:00Local timezone offset
os_versioniOS 18.0, Android 14OS version string
api-keyyour project keyProject authentication

Current SDKs send device_model automatically. Older SDK versions still work via screen_size. Country and city are resolved server-side from the request IP — you do not send them.

Matching uses two paths, in order:

  1. Same IP (last 48 hours) — look up recent quiz/paywall visits from this IP, then confirm among those candidates with the device fingerprint. device_model is preferred (stable between Chrome Client Hints on the web and the native app). screen_size is a tie-break, and the fallback when the web session has no stored model (typical for iOS Safari).
  2. Global fingerprint — if the IP changed (VPN, Wi-Fi vs cellular), match without IP using the same signals plus country. The candidate must be unique among sessions in the last 48 hours. If several users share the fingerprint, identify returns no user_id instead of guessing.

Fingerprint matching requires platform, timezone, and at least one of device_model or screen_size. os_version tightens the match. On the IP path, Android N from the app also matches frozen Chrome "Android 10". On the global path, OS version must match exactly.

Response format

A successful match returns:

{
  "success": 1,
  "user_id": "identified_user_guid",
  "match_method": "ip_device_fingerprint_with_model",
  "platform": "iOS"
}

match_method tells you how the user was resolved:

match_methodMeaning
ip_device_fingerprint_with_modelSame IP + device model
ip_device_fingerprint_with_osSame IP + screen size / OS
ip_device_fingerprintSame IP + screen size (no OS sent)
device_fingerprint_with_modelNo IP match; unique device model
device_fingerprint_with_osNo IP match; unique screen size / OS
ambiguous_fingerprintSeveral recent users share the fingerprint — no user_id
no_matchNo candidate in the last 48 hours

If no unique match is found, success is 0 (or the call returns null / invokes the error callback depending on the SDK).

Integration flow

After you have a user_id, link your paywall SDK and verify entitlement:

  1. setAdaptyProfileID / setRevenuecatProfileID / setQonversionProfileID
  2. Wait 1–2 seconds, then restore purchases in the subscription SDK
  3. hasActiveSubscription(user_id) to unlock content

Code examples

Web2Wave.shared.initialize(apiKey: 'your-api-key');

// No MMP — resolve user_id via deferred deeplink
final identificationData = await Web2Wave.shared.identify();

if (identificationData != null && identificationData['success'] == 1) {
  final userId = identificationData['user_id'] as String?;
  if (userId != null) {
    await Web2Wave.shared.setAdaptyProfileID(
      web2waveUserId: userId,
      adaptyProfileId: adaptyProfile.profileId!,
    );
    await Future.delayed(const Duration(seconds: 2));
    await Adapty().restorePurchases();
  }
}
💡

Run network calls off the main thread on Android (Java/Kotlin). Use a background coroutine with Dispatchers.IO or an ExecutorService.

Direct API usage

If you are not using an SDK, send the same headers manually:

curl -X GET "https://api.web2wave.com/api/user/identify" \
  -H "api-key: YOUR_API_KEY" \
  -H "User-Agent: MyApp/1.0 (iPhone; iOS 18.0)" \
  -H "platform: iOS" \
  -H "device_model: iPhone15,2" \
  -H "screen_size: 390x844" \
  -H "timezone: UTC+03:00" \
  -H "os_version: iOS 18.0"

User-Agent is required. Send device_model whenever the OS exposes it (Build.MODEL on Android, utsname.machine on iOS). screen_size remains useful as a fallback, especially for iOS web sessions.

See also: Direct web2wave API integration and GET /user/identify.

Related guides