Skip to content
Tolinku
Tolinku
Sign In Start Free
Analytics & Attribution · · 4 min read

Install Validation: Ensuring Accurate Attribution

By Tolinku Staff
|
Tolinku mobile attribution dashboard screenshot for analytics blog posts

Install validation is the process of verifying that a reported app install is genuine. Without validation, your attribution data includes fake installs from device farms, emulators, SDK spoofing, and other fraud techniques. Accurate attribution depends on knowing which installs are real.

This guide covers install validation techniques. For click injection specifically, see click injection detection. For attribution fraud broadly, see attribution fraud: detection and prevention guide.

Why Install Validation Matters

In a typical mobile ad campaign, 10-30% of reported installs may be fraudulent (according to industry estimates). If you spend $50,000 on a campaign that delivers 10,000 installs, and 2,000 of those are fake, your effective CPI is $6.25 instead of $5.00. More importantly, your attribution data is corrupted: you are optimizing campaigns based on data that includes fake users.

Validation Techniques

App Store Receipt Validation

Both Apple and Google provide receipt validation APIs that confirm an install came from the official app store.

iOS: App Store Receipt

The App Store receipt is a cryptographically signed file that proves the app was downloaded from the App Store:

func validateAppStoreReceipt() -> Bool {
    guard let receiptURL = Bundle.main.appStoreReceiptURL,
          FileManager.default.fileExists(atPath: receiptURL.path) else {
        return false // No receipt, not installed from App Store
    }

    guard let receiptData = try? Data(contentsOf: receiptURL) else {
        return false
    }

    // Send to your server for validation
    // Your server calls Apple's verifyReceipt endpoint
    return sendToServerForValidation(receiptData)
}

Server-side validation:

async function validateAppleReceipt(receiptData: string): Promise<boolean> {
  // Try production first
  let response = await fetch('https://buy.itunes.apple.com/verifyReceipt', {
    method: 'POST',
    body: JSON.stringify({ 'receipt-data': receiptData }),
  });

  let result = await response.json();

  // Status 21007 means sandbox receipt, retry with sandbox URL
  if (result.status === 21007) {
    response = await fetch('https://sandbox.itunes.apple.com/verifyReceipt', {
      method: 'POST',
      body: JSON.stringify({ 'receipt-data': receiptData }),
    });
    result = await response.json();
  }

  return result.status === 0; // 0 = valid
}

Android: Play Integrity API

The Play Integrity API (successor to SafetyNet) verifies that the app is genuine, the device is not rooted, and the install came from Google Play:

class IntegrityChecker(private val context: Context) {
    suspend fun checkIntegrity(): IntegrityResult {
        val integrityManager = IntegrityManagerFactory.create(context)

        val nonce = generateNonce() // Server-generated nonce

        val request = IntegrityTokenRequest.builder()
            .setNonce(nonce)
            .build()

        val response = integrityManager.requestIntegrityToken(request).await()
        val token = response.token()

        // Send token to your server for verification
        return verifyOnServer(token, nonce)
    }
}

SDK Signature Verification

Verify that attribution SDK events are coming from your actual app, not from a spoofed SDK:

interface InstallEvent {
  appId: string;
  sdkVersion: string;
  timestamp: number;
  signature: string;
  deviceInfo: DeviceInfo;
}

function verifySDKSignature(event: InstallEvent, secretKey: string): boolean {
  const payload = `${event.appId}:${event.timestamp}:${event.deviceInfo.deviceId}`;
  const expectedSignature = hmacSHA256(payload, secretKey);
  return event.signature === expectedSignature;
}

Server-Side Install Verification

Verify installs on your server by checking multiple signals:

interface ValidationResult {
  valid: boolean;
  score: number; // 0-100 confidence score
  flags: string[];
}

function validateInstall(event: InstallEvent): ValidationResult {
  const flags: string[] = [];
  let score = 100;

  // Check 1: Device ID format
  if (!isValidDeviceIdFormat(event.deviceInfo.deviceId)) {
    flags.push('invalid_device_id');
    score -= 40;
  }

  // Check 2: Timestamp sanity
  const now = Date.now() / 1000;
  if (Math.abs(now - event.timestamp) > 3600) {
    flags.push('timestamp_drift');
    score -= 20;
  }

  // Check 3: Known emulator signatures
  if (isEmulatorSignature(event.deviceInfo)) {
    flags.push('emulator_detected');
    score -= 50;
  }

  // Check 4: App store receipt
  if (!event.storeReceipt) {
    flags.push('no_store_receipt');
    score -= 30;
  }

  // Check 5: IP reputation
  if (isDataCenterIP(event.deviceInfo.ip)) {
    flags.push('datacenter_ip');
    score -= 40;
  }

  // Check 6: Duplicate device ID
  if (isDuplicateDevice(event.deviceInfo.deviceId)) {
    flags.push('duplicate_device');
    score -= 30;
  }

  return {
    valid: score >= 50,
    score: Math.max(0, score),
    flags
  };
}

Fraud Signals

Device-Level Signals

Signal Indicates
Rooted/jailbroken device Possible emulator or tampered environment
Emulator detection Device farms
Abnormal screen resolution Non-standard device or emulator
Missing sensors (accelerometer, gyroscope) Emulator
Time zone mismatch (device vs. IP) VPN or location spoofing
Battery always at 100% Emulator

Behavioral Signals

Signal Indicates
No post-install activity Bot install (never used)
Identical user agent across many installs Device farm
Install at 3 AM local time Automated install
Instant permission grants Bot (real users take time to decide)
No scroll or gesture events SDK spoofing (no real user)

Network Signals

Signal Indicates
Data center IP Server-based fraud
VPN/proxy IP Location masking
Same IP for many installs Device farm
IP country does not match targeting Wrong geography

Emulator Detection

Device farms use emulators to generate fake installs at scale. Common detection techniques:

fun isEmulator(): Boolean {
    return (Build.FINGERPRINT.startsWith("generic")
        || Build.FINGERPRINT.startsWith("unknown")
        || Build.MODEL.contains("google_sdk")
        || Build.MODEL.contains("Emulator")
        || Build.MODEL.contains("Android SDK built for x86")
        || Build.BOARD == "QC_Reference_Phone"
        || Build.MANUFACTURER.contains("Genymotion")
        || Build.HOST.startsWith("Build")
        || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
        || "google_sdk" == Build.PRODUCT)
}

Note: Some legitimate users use emulators (developers testing their own apps). Consider the context before rejecting all emulator installs.

Handling Invalid Installs

When validation fails, you have three options:

  1. Reject immediately. Do not attribute the install. Send a rejection postback to the ad network.
  2. Flag for review. Mark the install as suspicious and continue attribution. Review flagged installs periodically.
  3. Quarantine. Hold the install in a pending state. If the user shows real engagement within 24-48 hours, accept it. If not, reject it.

The quarantine approach balances fraud prevention with false positive risk:

async function handleSuspiciousInstall(event: InstallEvent, validation: ValidationResult) {
  if (validation.score < 20) {
    // Very likely fraud, reject immediately
    await rejectInstall(event, validation.flags);
  } else if (validation.score < 50) {
    // Suspicious, quarantine
    await quarantineInstall(event, validation.flags);

    // Check engagement after 48 hours
    scheduleEngagementCheck(event.deviceInfo.deviceId, 48 * 3600);
  } else {
    // Probably legitimate, accept
    await acceptInstall(event);
  }
}

Tolinku for Install Tracking

Tolinku's analytics track deep link clicks with server-side validation, providing a clean source of truth for click-to-install attribution. Configure analytics in the Tolinku dashboard.

For click injection, see click injection detection. For mobile attribution, see mobile attribution: a developer's guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.