Docs

Install Trust for Android

From nothing to a conversion that fires only for verified installs.

What it is

Install Trust is an Android SDK and a hosted service. The SDK checks each install with Google Play Integrity and tells your app a verdict. Your app fires its own conversion for a verified install, through the analytics or attribution SDK it already ships.

The console lists every install with its verdict, the reasons behind it and its history.

Get access

Sign in at app.adhedge.io with your email. We email a link; there is no password. Your first sign-in creates the account. Name your organization and you own it.

That gives you apps, SDK setup, Overview and Installs. Reconciliation, evidence packs, members and API tokens are opened per workspace: ask through the form.

Working with a coding agent? Add the MCP server https://api.adhedge.io/mcp. It signs you in through the browser, registers the app and returns the same lines SDK setup shows. Its setup answer ends at the manifest, so have the agent follow Code and Your conversion from this page.

Register your app

  1. In the console choose New app. Owners and admins can.
  2. Enter a name and the package name, exactly as in Play Console. It is your applicationId.
  3. The console opens SDK setup for the app. Under Install it shows the routing key, with the repository line, the dependency line and the manifest entry ready to copy.

The routing key names your app. It is public and ships in your APK.

Play Console and Google Cloud

Play Integrity issues each install's token under a Cloud project linked to your app. Until SDK setup has a decode path saved, no install is attested and none becomes TRUSTED.

  1. In Play Console, select your app. Go to App integrity, then Play Integrity API. Choose Link Cloud project and pick or create a project.
  2. Copy that project's number. Digits only.
  3. In SDK setup, under Decode, choose Your own Cloud project with Google decode and paste the number.
  4. In Google Cloud, enable the Play Integrity API in that project. Setup links to the page.
  5. In the same project, grant the role roles/playintegrity.user to the account your Setup screen shows. Setup has a copy button for its address.
  6. Save. Phones pick the project up with their next configuration request, within an hour.

Decode reads No decode yet until the first token is read, then the time of the last one. Choose Google decode: it is the path the service decodes.

Signing certificate

  1. In Play Console, open App integrity and find the app signing key certificate. Copy its SHA-256 fingerprint.
  2. In SDK setup, under Signing certificates, choose Add certificate and paste it. Colons and uppercase are fine.

Take the fingerprint from Play Console. SDK setup also lists certificates it has observed, and a repackaged build's certificate shows up there too.

An install signed by a certificate you have not registered carries a red flag for it. Your app reads Waiting for first install until the first token from a build Play recognizes, signed by a registered certificate.

Gradle

The SDK is on AdHedge's own Maven repository. Add it in settings.gradle.kts, beside the two your project already has. The content block scopes it: Gradle asks this repository for the io.adhedge group only.

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://maven.adhedge.io") {
            content { includeGroup("io.adhedge") }
        }
    }
}

In settings.gradle, for the Groovy DSL:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.adhedge.io")
            content { includeGroup("io.adhedge") }
        }
    }
}

The dependency, in your app module's build.gradle.kts:

dependencies {
    implementation("io.adhedge:install-trust:0.5.0")
}

Or in its build.gradle:

dependencies {
    implementation 'io.adhedge:install-trust:0.5.0'
}

SDK setup gives the same line. Every published version is in the Maven metadata. The SDK's own dependencies come from google() and mavenCentral().

RequirementValue
minSdk23
compileSdk34 or higher
Kotlin2.1 or higher for Kotlin callers. Java callers need no Kotlin compiler.

Manifest

Add your routing key to AndroidManifest.xml, inside <application>:

<meta-data
    android:name="io.adhedge.installtrust.ROUTING_KEY"
    android:value="itk_..." />

Write the key as a literal android:value. An android:resource, a number or a boolean reads as no key.

What the SDK and its libraries merge into your manifest:

Code

There is no init call. The SDK starts itself through androidx.startup. If your app removes the startup provider, remove the SDK's meta-data line with tools:node="remove" and call InstallTrust.start(this) in Application.onCreate.

The verdict listener

Register a listener in Application.onCreate. On a final TRUSTED for a new install, fire your conversion once.

import android.app.Application
import io.adhedge.installtrust.core.InstallTrust
import io.adhedge.installtrust.core.Verdict

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        InstallTrust.addVerdictListener { verdict, isFinal ->
            if (verdict != Verdict.TRUSTED || !isFinal || !InstallTrust.isNewInstall()) return@addVerdictListener
            @Suppress("DEPRECATION") // the flags overload needs API 33
            val installed = packageManager.getPackageInfo(packageName, 0).firstInstallTime
            val prefs = getSharedPreferences("install_trust_conversion", MODE_PRIVATE)
            if (prefs.getLong("fired_for_install", 0L) == installed) return@addVerdictListener
            if (!prefs.edit().putLong("fired_for_install", installed).commit()) return@addVerdictListener
            sendVerifiedInstall() // your analytics or attribution SDK's event call
        }
    }
}

In Java:

import android.app.Application;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import io.adhedge.installtrust.core.InstallTrust;
import io.adhedge.installtrust.core.Verdict;

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        InstallTrust.addVerdictListener((verdict, isFinal) -> {
            if (verdict != Verdict.TRUSTED || !isFinal || !InstallTrust.isNewInstall()) return;
            long installed;
            try {
                installed = getPackageManager().getPackageInfo(getPackageName(), 0).firstInstallTime;
            } catch (PackageManager.NameNotFoundException e) {
                return;
            }
            SharedPreferences prefs = getSharedPreferences("install_trust_conversion", MODE_PRIVATE);
            if (prefs.getLong("fired_for_install", 0L) == installed) return;
            if (!prefs.edit().putLong("fired_for_install", installed).commit()) return;
            sendVerifiedInstall(); // your analytics or attribution SDK's event call
        });
    }
}

Verdicts

Verdict has six values. The listener's second argument, isFinal, says whether this one is final. On the device a final verdict is never replaced.

ValueFinalMeansYour app
PROVISIONAL_TRUSTEDNoThe first answer, before a Play Integrity token is decoded.Wait.
PROVISIONAL_SUSPECTNoThe first answer, before a Play Integrity token is decoded.Wait.
TRUSTEDYesPlay verified the app and its license.Fire your conversion when isNewInstall() is true.
UNVERIFIEDYesOne red flag, or Play couldn't confirm it. Also what the SDK keeps when the service refuses the token request or has not answered it after seven days.Fire nothing.
SUSPECTYesRed flags from two different checks, on a Non-Play install or an existing user.Fire nothing.
UNTRUSTEDYesRed flags from two different checks, on a new install from Play or an unknown source.Fire nothing. The one value the ad gate can close for.

The enum is closed for the 1.x line, so an exhaustive when stays safe. InstallTrust.verdict() returns the last verdict held, provisional or final, and null until the first.

The console shows three verdicts, and shows UNTRUSTED as SUSPECT with a badge. Its caption: TRUSTED: Play verified the app and its license. One in-app behavior flag never downgrades a phone Play recognized and licensed with strong device integrity. UNVERIFIED: one red flag, or Play couldn't confirm it. SUSPECT: red flags from two different checks, such as USB debugging plus no taps on day 0. Excludes Non-Play, existing users and sampled-out installs.

The console can revise an install later from its first two days of behavior. The device keeps the final verdict it was told.

isNewInstall()

True when the SDK recorded this install as new: the app had not been updated since it was installed, or had been installed no more than 48 hours before the SDK first ran. An existing user who updates into a build with the SDK still gets a verdict, and this is false for them. It is also false until the first verdict in the process that decides it. Read it inside the listener.

shouldRequestAds()

Call it before each ad request. It is local and constant time, safe on any thread. It returns false only for an install whose final verdict is UNTRUSTED under a blocking gate, a mode the service sets per app. In every other case it is true, including before the SDK starts. Read it when you need it and never cache it.

if (InstallTrust.shouldRequestAds()) {
    loadAd()
}

The console states the same rule in its own words: Gated: new installs from Play or Unknown with a final SUSPECT verdict, which takes red flags from two different checks. Rooting and no device integrity are one check. An unregistered signing key is its own check, so a build Play didn't recognize signed with an unregistered key counts as two. Never Non-Play, existing users or pending installs. Phones pick up changes within an hour.

label(name)

Counts a label on the device and sends nothing. Calling it changes nothing in the console.

Threads

Your conversion

The console and these docs call the event verified_install. Use that name in every tool. With Firebase:

private fun sendVerifiedInstall() {
    FirebaseAnalytics.getInstance(this).logEvent("verified_install", null)
}

With AppsFlyer, Adjust, Meta or TikTok, call that SDK's own event method with the same name. Adjust takes the event token you create for it.

  1. In Firebase, mark verified_install as a key event. In your attribution tool, map it to your ad networks.
  2. In the ad network, import it as a conversion and set it as the conversion your install campaigns optimize for.
  3. In SDK setup's checklist, mark Set verified_install as your primary conversion done.

Changing a campaign's conversion restarts its learning. Bidding steers from about 10 real players a day.

Timeline and failure modes

WhenWhat happens
First launchThe SDK asks the service for its configuration, sends one signals request and your listener hears a provisional verdict. It then asks Play for a token and sends it, and your listener hears the final verdict. When the service needs no token, the first answer is already final.
How longThe SDK gives no upper bound. The token step alone can wait several minutes. Never hold your UI for a verdict.
OfflineNothing is decided. Requests wait on disk, at most 20 for seven days, and are sent again with a backoff from one minute to one hour, in this launch and later ones.
Play gives no tokenThe SDK asks again in later launches: three asks per install at most, inside seven days. With no token the device keeps its provisional verdict and your listener hears no final one. The console closes the install with a verdict at seven days.
Seven daysA token request the service refuses, or has not answered after seven days by the device's clock, ends as a final UNVERIFIED.
ReinstallA new install. The SDK checks it again, and your marker lets it fire again.
Existing users after an updateRecorded as existing when the app was updated since its install and more than 48 hours had passed. They get a verdict, isNewInstall() is false, their touches are not observed and they are never billed.
Missing or malformed keyThe SDK idles for the process: it collects nothing and sends nothing. No crash, and no install in the console.
Well-formed key the service does not knowThe service answers not found and the SDK goes silent for 24 hours, across relaunches, until that time passes or the app is updated. No install in the console.
No decode path or no Cloud projectNo token is asked for. Installs are not attested and none becomes TRUSTED.
SDK paused by an ownerNothing is collected or sent while the pause stands.

Test it

The published SDK logs nothing and has no debug flag or test mode. On the device your listener is the window. Log from it in a debug build:

InstallTrust.addVerdictListener { verdict, isFinal ->
    Log.d("MyApp", "Install Trust: $verdict final=$isFinal new=${InstallTrust.isNewInstall()}")
}

Reconciliation

Upload any network's install report. The console compares it with the first opens the SDK saw. It is opened per workspace.

ColumnAccepted headersRequired
DateDay, Date, Event dateYes
InstallsInstalls, App installs, Conversions, Conv., All conv.Yes
CampaignCampaign, Campaign name, Source / mediumNo
CostAny header starting with CostNo

What the SDK sends

Your Play Data safety answers, with every field, its Android source and how long it is kept: data safety. The policy for the SDK and its service: SDK privacy policy.

The SDK talks to one host, i.adhedge.io, over TLS only. Add it to a network allow-list if you keep one. The Play Integrity token and the install referrer come from Google Play on the device.

One signals request is built per install. A value the Android version cannot provide, or whose read fails, is sent as null.

At each start the SDK also asks the service for its configuration. That request carries the package name, the signing certificate hash, the SDK version, the API level, the version code and the existing-user flag. Every request carries your routing key.

Deletion. To delete an install's data, send us its install ID through the form. The ID is on the install's page in the console. The console has no delete button, and the SDK has no call that returns the ID on the device.

For code review

How touches are counted

One Application.ActivityLifecycleCallbacks registration, and a wrapper around the Window.Callback of each Activity created after the SDK starts. No View.OnTouchListener on your views, no accessibility service, no input injection. Touches in a dialog's or a popup's own window are not seen.

On the dispatch path the wrapper reads the event's action and nothing else of it. When a touch begins it reads a clock and adds to a count and two sums. Then it calls the callback that was there before, whose result and exceptions pass through unchanged. It allocates nothing per event, keeps no reference to the event, takes no lock and does no I/O.

Your stack traces through touch dispatch show one frame of that wrapper: io.adhedge.installtrust.signals.TouchHook$Callback.dispatchTouchEvent, or whatever name your R8 gives the class.

Remove it

  1. Point your campaigns at another conversion first. Once the listener is gone, verified_install stops.
  2. Delete the listener, any shouldRequestAds() and label() calls, the manifest entry, the dependency and the repository block.
  3. Ship the build. The SDK's files stay in the app's private storage until the app's data is cleared or the app is uninstalled.

What was already sent is kept for the windows on the data safety page, or until you ask for deletion.

Get help

The form is the contact. It reaches the team by email.