{"id":1737,"date":"2026-07-13T13:00:00","date_gmt":"2026-07-13T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1737"},"modified":"2026-03-07T03:50:05","modified_gmt":"2026-03-07T08:50:05","slug":"install-validation-attribution","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/install-validation-attribution\/","title":{"rendered":"Install Validation: Ensuring Accurate Attribution"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers install validation techniques. For click injection specifically, see <a href=\"https:\/\/tolinku.com\/blog\/click-injection-detection\/\">click injection detection<\/a>. For attribution fraud broadly, see <a href=\"https:\/\/tolinku.com\/blog\/attribution-fraud-detection\/\">attribution fraud: detection and prevention guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Install Validation Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a typical mobile ad campaign, 10-30% of reported installs may be fraudulent (<a href=\"https:\/\/www.appsflyer.com\/state-of-mobile-app-fraud\/\" rel=\"nofollow noopener\" target=\"_blank\">according to industry estimates<\/a>). 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Validation Techniques<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">App Store Receipt Validation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Both Apple and Google provide receipt validation APIs that confirm an install came from the official app store.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>iOS: App Store Receipt<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/developer.apple.com\/documentation\/appstorereceipts\" rel=\"nofollow noopener\" target=\"_blank\">App Store receipt<\/a> is a cryptographically signed file that proves the app was downloaded from the App Store:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func validateAppStoreReceipt() -&gt; Bool {\n    guard let receiptURL = Bundle.main.appStoreReceiptURL,\n          FileManager.default.fileExists(atPath: receiptURL.path) else {\n        return false \/\/ No receipt, not installed from App Store\n    }\n\n    guard let receiptData = try? Data(contentsOf: receiptURL) else {\n        return false\n    }\n\n    \/\/ Send to your server for validation\n    \/\/ Your server calls Apple&#39;s verifyReceipt endpoint\n    return sendToServerForValidation(receiptData)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Server-side validation:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function validateAppleReceipt(receiptData: string): Promise&lt;boolean&gt; {\n  \/\/ Try production first\n  let response = await fetch(&#39;https:\/\/buy.itunes.apple.com\/verifyReceipt&#39;, {\n    method: &#39;POST&#39;,\n    body: JSON.stringify({ &#39;receipt-data&#39;: receiptData }),\n  });\n\n  let result = await response.json();\n\n  \/\/ Status 21007 means sandbox receipt, retry with sandbox URL\n  if (result.status === 21007) {\n    response = await fetch(&#39;https:\/\/sandbox.itunes.apple.com\/verifyReceipt&#39;, {\n      method: &#39;POST&#39;,\n      body: JSON.stringify({ &#39;receipt-data&#39;: receiptData }),\n    });\n    result = await response.json();\n  }\n\n  return result.status === 0; \/\/ 0 = valid\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Android: Play Integrity API<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/developer.android.com\/google\/play\/integrity\" rel=\"nofollow noopener\" target=\"_blank\">Play Integrity API<\/a> (successor to SafetyNet) verifies that the app is genuine, the device is not rooted, and the install came from Google Play:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">class IntegrityChecker(private val context: Context) {\n    suspend fun checkIntegrity(): IntegrityResult {\n        val integrityManager = IntegrityManagerFactory.create(context)\n\n        val nonce = generateNonce() \/\/ Server-generated nonce\n\n        val request = IntegrityTokenRequest.builder()\n            .setNonce(nonce)\n            .build()\n\n        val response = integrityManager.requestIntegrityToken(request).await()\n        val token = response.token()\n\n        \/\/ Send token to your server for verification\n        return verifyOnServer(token, nonce)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">SDK Signature Verification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verify that attribution SDK events are coming from your actual app, not from a spoofed SDK:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface InstallEvent {\n  appId: string;\n  sdkVersion: string;\n  timestamp: number;\n  signature: string;\n  deviceInfo: DeviceInfo;\n}\n\nfunction verifySDKSignature(event: InstallEvent, secretKey: string): boolean {\n  const payload = `${event.appId}:${event.timestamp}:${event.deviceInfo.deviceId}`;\n  const expectedSignature = hmacSHA256(payload, secretKey);\n  return event.signature === expectedSignature;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Server-Side Install Verification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verify installs on your server by checking multiple signals:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface ValidationResult {\n  valid: boolean;\n  score: number; \/\/ 0-100 confidence score\n  flags: string[];\n}\n\nfunction validateInstall(event: InstallEvent): ValidationResult {\n  const flags: string[] = [];\n  let score = 100;\n\n  \/\/ Check 1: Device ID format\n  if (!isValidDeviceIdFormat(event.deviceInfo.deviceId)) {\n    flags.push(&#39;invalid_device_id&#39;);\n    score -= 40;\n  }\n\n  \/\/ Check 2: Timestamp sanity\n  const now = Date.now() \/ 1000;\n  if (Math.abs(now - event.timestamp) &gt; 3600) {\n    flags.push(&#39;timestamp_drift&#39;);\n    score -= 20;\n  }\n\n  \/\/ Check 3: Known emulator signatures\n  if (isEmulatorSignature(event.deviceInfo)) {\n    flags.push(&#39;emulator_detected&#39;);\n    score -= 50;\n  }\n\n  \/\/ Check 4: App store receipt\n  if (!event.storeReceipt) {\n    flags.push(&#39;no_store_receipt&#39;);\n    score -= 30;\n  }\n\n  \/\/ Check 5: IP reputation\n  if (isDataCenterIP(event.deviceInfo.ip)) {\n    flags.push(&#39;datacenter_ip&#39;);\n    score -= 40;\n  }\n\n  \/\/ Check 6: Duplicate device ID\n  if (isDuplicateDevice(event.deviceInfo.deviceId)) {\n    flags.push(&#39;duplicate_device&#39;);\n    score -= 30;\n  }\n\n  return {\n    valid: score &gt;= 50,\n    score: Math.max(0, score),\n    flags\n  };\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Fraud Signals<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Device-Level Signals<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Signal<\/th>\n<th>Indicates<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Rooted\/jailbroken device<\/td>\n<td>Possible emulator or tampered environment<\/td>\n<\/tr>\n<tr>\n<td>Emulator detection<\/td>\n<td>Device farms<\/td>\n<\/tr>\n<tr>\n<td>Abnormal screen resolution<\/td>\n<td>Non-standard device or emulator<\/td>\n<\/tr>\n<tr>\n<td>Missing sensors (accelerometer, gyroscope)<\/td>\n<td>Emulator<\/td>\n<\/tr>\n<tr>\n<td>Time zone mismatch (device vs. IP)<\/td>\n<td>VPN or location spoofing<\/td>\n<\/tr>\n<tr>\n<td>Battery always at 100%<\/td>\n<td>Emulator<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Behavioral Signals<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Signal<\/th>\n<th>Indicates<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>No post-install activity<\/td>\n<td>Bot install (never used)<\/td>\n<\/tr>\n<tr>\n<td>Identical user agent across many installs<\/td>\n<td>Device farm<\/td>\n<\/tr>\n<tr>\n<td>Install at 3 AM local time<\/td>\n<td>Automated install<\/td>\n<\/tr>\n<tr>\n<td>Instant permission grants<\/td>\n<td>Bot (real users take time to decide)<\/td>\n<\/tr>\n<tr>\n<td>No scroll or gesture events<\/td>\n<td>SDK spoofing (no real user)<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Network Signals<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Signal<\/th>\n<th>Indicates<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Data center IP<\/td>\n<td>Server-based fraud<\/td>\n<\/tr>\n<tr>\n<td>VPN\/proxy IP<\/td>\n<td>Location masking<\/td>\n<\/tr>\n<tr>\n<td>Same IP for many installs<\/td>\n<td>Device farm<\/td>\n<\/tr>\n<tr>\n<td>IP country does not match targeting<\/td>\n<td>Wrong geography<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Emulator Detection<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Device farms use emulators to generate fake installs at scale. Common detection techniques:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">fun isEmulator(): Boolean {\n    return (Build.FINGERPRINT.startsWith(&quot;generic&quot;)\n        || Build.FINGERPRINT.startsWith(&quot;unknown&quot;)\n        || Build.MODEL.contains(&quot;google_sdk&quot;)\n        || Build.MODEL.contains(&quot;Emulator&quot;)\n        || Build.MODEL.contains(&quot;Android SDK built for x86&quot;)\n        || Build.BOARD == &quot;QC_Reference_Phone&quot;\n        || Build.MANUFACTURER.contains(&quot;Genymotion&quot;)\n        || Build.HOST.startsWith(&quot;Build&quot;)\n        || Build.BRAND.startsWith(&quot;generic&quot;) &amp;&amp; Build.DEVICE.startsWith(&quot;generic&quot;)\n        || &quot;google_sdk&quot; == Build.PRODUCT)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note: Some legitimate users use emulators (developers testing their own apps). Consider the context before rejecting all emulator installs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Invalid Installs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When validation fails, you have three options:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Reject immediately.<\/strong> Do not attribute the install. Send a rejection postback to the ad network.<\/li>\n<li><strong>Flag for review.<\/strong> Mark the install as suspicious and continue attribution. Review flagged installs periodically.<\/li>\n<li><strong>Quarantine.<\/strong> Hold the install in a pending state. If the user shows real engagement within 24-48 hours, accept it. If not, reject it.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The quarantine approach balances fraud prevention with false positive risk:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function handleSuspiciousInstall(event: InstallEvent, validation: ValidationResult) {\n  if (validation.score &lt; 20) {\n    \/\/ Very likely fraud, reject immediately\n    await rejectInstall(event, validation.flags);\n  } else if (validation.score &lt; 50) {\n    \/\/ Suspicious, quarantine\n    await quarantineInstall(event, validation.flags);\n\n    \/\/ Check engagement after 48 hours\n    scheduleEngagementCheck(event.deviceInfo.deviceId, 48 * 3600);\n  } else {\n    \/\/ Probably legitimate, accept\n    await acceptInstall(event);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Install Tracking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/analytics\">Tolinku&#39;s analytics<\/a> track deep link clicks with server-side validation, providing a clean source of truth for click-to-install attribution. Configure analytics in the <a href=\"https:\/\/tolinku.com\/docs\/concepts\/attribution\/\">Tolinku dashboard<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For click injection, see <a href=\"https:\/\/tolinku.com\/blog\/click-injection-detection\/\">click injection detection<\/a>. For mobile attribution, see <a href=\"https:\/\/tolinku.com\/blog\/mobile-attribution-developers-guide\/\">mobile attribution: a developer&#39;s guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Validate app installs for accurate attribution. Use receipt validation, server-side verification, and fraud signals to maintain data quality.<\/p>\n","protected":false},"author":2,"featured_media":1736,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Install Validation: Ensuring Accurate Attribution","rank_math_description":"Validate app installs for accurate attribution. Use receipt validation, server-side verification, and fraud signals to maintain data quality.","rank_math_focus_keyword":"install validation","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-install-validation-attribution.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-install-validation-attribution.png","footnotes":""},"categories":[14],"tags":[37,526,28,20,126,525,69,93],"class_list":["post-1737","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-app-stores","tag-attribution","tag-deep-linking","tag-fraud-detection","tag-install-validation","tag-mobile-development","tag-security"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1737","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1737"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1737\/revisions"}],"predecessor-version":[{"id":2692,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1737\/revisions\/2692"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1736"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1737"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1737"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1737"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}